The purpose of this document is to outline the following
- Why asynchronous tests can pass even though it is not testing anything
- How to actually test asynchronous code
- In one word: javascript
- Javascript is an asynchronous, non-blocking language.
- This means that it keeps going and waits on no function.
- So what does your unit test actually do?
- It runs a block of code and looks for failures.
- In the absence of failures, your unit test has passed.
- But javascript is non-blocking! And you are testing asynchronous code.
- Ideally, this is the behaviour you want:
EXPECTED BEHAVIOUR
|
Main thread
|
| --> Unit test
| |
| | --- Async call -> |
| | |
| | <-- Response ---- |
| <------ |
|
- But this flow requires the unit test to wait for the response to come back.
- Javascript will run to the end of the unit test without waiting for a response.
REALITY
|
Main thread
|
| --> Unit test
| |
| | --- Async call -> |
| | |
| | |
| <------ | |
| |
| ??? <-- Response ---- |
|
- The response came back after the unit test has finished executing.
- Therefore, the unit test never tested the response.
- In the absence of checking the response, the unit test never threw a failure.
- As a result, this unit test has resulted in false pass.
- What you want to do is wait for the asynchronous response to come back and finish the unit test only after checking the response.
- Let's use an example of asynchronous test that does not wait for the response
...
// THIS DOES NOT CORRECTLY TEST THE RESPONSE
it('returns success', () => {
dispatch(actions.goPewPew({
dependencies: {
...Dependencies,
waveClient: MockWaveClient('tiger-pistol', 'shoot', payload),
},
})).then(() => {
expect(store.getActions()).toEqual(
[{ type: actionTypes.REACH_FOR_THE_SKY }],
);
});
...
- Jasmine (and Chai as well) has an optional
done()
callback parameter in theit()
andbeforeEach()
blocks. - The callback
done()
makes the unit test thread stay alive untildone()
is called. - If it is never called, there is a default timeout and the unit test will fail with a specific timeout error.
- The
done()
callback is powerful because you can put it in the response to an asynchronous call.
...
// All fixed now!
it('returns success', (done) => {
dispatch(actions.goPewPew({
dependencies: {
...Dependencies,
waveClient: MockWaveClient('tiger-pistol', 'shoot', payload),
},
})).then(() => {
expect(store.getActions()).toEqual(
[{ type: actionTypes.REACH_FOR_THE_SKY }],
);
done(); //<=== The unit test is not finished until it reaches here!
});
...
- Now the unit test thread will wait until the
done()
is called in thethen()
block. - In addition, this unit test is more robust because it will fail if a response never came back. It would timeout because
done()
was never called.
- If you have a
then()
block in your test, you are making an asynchronous call. - Even if you mock the response/client/magic out, the test still needs to have the
done()
callback.
- Jasmine has a optional callback parameter for
it()
calleddone()
. - Add it to your
it()
function callback. - Call it in your asynchronous response.