(Testing Promises in Angular.)
The one used in Angular testing — yes that one. What is is flushing exactly? And more importantly — when do we use it?
We use flushMicrotasks inside of a test
zoneto control the execution of all tasks in the microtask queue.
Consider the following Stackblitz:
(I got some feedback that on mobile browsers Stackblitz is hard to use — so see gist below if that’s the case for you)
There is an
AuthService that has the
getUser method. It returns a
hello.component should redirect to
Router when user is the empty user. Simple enough, right. Turns out that test fails.
If you open the Stackblitz and at the
hello.component.spec.ts and comment out or delete the
pending call on line 27, and run it the test breaks. It breaks even though we’ve specifically mocked out the
AuthService to return the resolved promise of the empty user. It follows the logic and the
HelloComponent should redirect. And it does. On line 34 — we log out to console that we actually call the
navigate method on the mocked router. Wait what? Is jasmine broken? What happens — we want to have been called and it has — so why jasmine u no see?
The answer is java script and the event loop. See Jake Archibald’s great article explaining what a microtask is and how it differs from a task (aka macrotask in angular/zone.js) Know it, learn it, love it as it goes.
In short tasks must wait for the browser to finish a loop (as in event loop) before they get the chance to run some JS, while microtasks get to run JS immediately after the call stacks is empty and if more microtasks are scheduled from one microtask — they get executed immediately after. Things like Promise-s and MutationObserver-s. Here’s a glimpse of the Promise polyfill implementation from core.js which gives a good idea of how Promise is implemented. It steps on the microtask polyfill, which in turn uses the task polyfill.
Here’s a rundown of what happens, step by step in our failing test. Keep in mind java script is single threaded so everything happens synchronously.
- Jasmine calls our test and
itstarts i.e. pushes its frame on the call stack.
- We define our mocks i.e. mutate the jasmine mock objects instructing them how to behave (they push and pop their frames on the call stack also — but that’s not interesting for our case)
- We call
component.login()so now it gets on the call stack.
- Now inside
loginwe synchronously call
authService.getUser(which is why the first test passes). It pushes its frame.
- The mock kicks in and constructs a Promise and immediately returns it. The Promise is scheduled as a microtask. The mock returns. It’s frame is popped out of the call stack and we are back in the
loginframe has nothing else to do so it returns and get’s popped off the stack
- Now we are in the
itframe where the
expectfollows and gets executed and fails the test. At this point the promise has not had a chance to execute its callback because it is waiting for the call stack to be empty.
- After call stack drains, promise is pushed on the call stack.
- It does its thing and logs out ‘navigating’ to the console.
Now it’s clear why the test failed —
expect is executed before the
router.navigate. But what can we do to make sure all the microtasks are executed before the test gets to finish?
setTimeout? That might work (it will) but can we do better?
How about giving instruction to the microtask queue to flush? Enter
flushMicrotasks. We can use the
fakeAsync to execute the test inside a zone and we can have control over the execution of microtasks (and macrotasks). That’s what we do in the next Stackblitz (or gist below):
The difference here is right after step 6. At that time we call
flushMicrotasks (line 30 or line 7 in gist). And that takes the
resolve(User.Empty) and pushes it onto the call stack which triggers the
router.navigate('/login') and at the time the
expect gets called we have called
navigate and it passes the test!
There you go, we use
flushMicrotasks inside of a test
zone to control the execution of all tasks in the
For the longest time I really did not understand the
fakeAsync and what it’s for. I hope I’ve managed to share some of the revelation of flushMicrotasks. It happened to me when I was having a very similar failing test to what is seen in the initial example. And bumping my head for a non-trivial amount of time. The thing that helped me was the knowledge that promise uses microtasks. So thanks to Jake Archibald for his great article. I strongly encourage you to look into the event loop and how it works. It’s proven to be a core piece of knowledge again and again.
Another important Angular piece of the puzzle is
zonejs You may want to look into the Max Koretskyi, aka Wizard article. (thumbs up, Max and all the rest of the authors, for the Angular in depth blog).
Thanks for reading. :) (and sorry it’s been so long since the last ng-gotcha but I’ve been busy)