How Promise.race Solved a Problem In Our Async CI Testing
Javascript is really a great language when it comes to async programming. But it is not always a walk in the park to test async code.
It is a good idea in unit testing to try to test at the highest possible level — focusing on testing of the module’s API from an outside/usage perspective. That way, we ensure that it does what it should, but we don’t care exactly how — making refactoring of the module almost zero work when it comes to updating tests. However, this often increases the amount of async testing.
In cases where the calls to test are straightforward promises, you await those and test on the result. But this is not always the case. For instance when callbacks have to be used, like when subscribing to a message bus, there is no promise to await.
A first try — with dummy delay
A common pattern in those cases is to have an arbitrary delay after the tested call. In Javascript we can define the simple wait/delay function first, as a handy utility:
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
And then use that to “make sure” we got the published message:
This trivial approach has a couple of problems though:
- It causes the testing to take way longer than necessary (imagine that hundreds of similar tests are run).
- It makes assumptions about the performance and scheduling in the environment where the tests are run, making it possible for tests to actually fail when they shouldn’t in cases of unfortunate scheduling and delays in the execution.
Second try —a triggerPromise
To solve this, we create a utility function that we can call triggerPromise
:
The utility function returns a new Promise
along with the resolve
function for that promise. Let's use it and see how it does away with the long arbitrary wait in our test:
We are now effectively signaling at the precise time when we have received the message, along with awaiting that signal, instead of guessing and overestimating.
Wrapping it all up with a race against a timeout
However, if the tested module has some error in this async functionality, then we don’t want to get stuck and wait forever because it would not work well with CI. So the next desired utility would be something like “await this thing but with a timeout”. Since there is a sibling to the much more used Promise.all
, namely Promise.race
, we could utilize that like this:
So now, instead of only doing await promise
in our tests, we use await asyncWithTimeout(promise)
, and voila — no more stalled CI because of newly introduced bugs in our async behaviours!
Oh yes, we need to have an example of this in use, completing our small async test: