Current section: Testing Asynchronous Code 5 exercises

Reliably Wait for Async Operations


00:00 To write this test reliably, we need a utility function called wait_for that will allow us to await a state transition when a greeting message is pushed to the notifications array. So let's implement this function. We have the most of its implementation right here, so I'm just going to use it.

00:14 The way the wait_for function works is not the point of this exercise, but let's still go through what it does. So it accepts a callback as an argument. This is something that we want to retry. For example, our assertion. And the maximum retries is a number,

00:32 so we will have five retries until this function rejects. The wait_for function itself returns a promise, which is nice because it's going to take time until it retries our callback. And then it has a while loop that just keeps retrying by calling our callback. And if it doesn't throw an error, it simply returns and stops the loop and resolves the promise return from the wait_for function.

00:51 But if it does throw an error, it will check if this isn't the maximum retry, so the final retry. If that's the case, it will propagate this error to the test, and the test will fail. But if we still need to keep retrying, it simply awaits the promise, which is a timeout for 220 milliseconds. This is very similar to a sleep function,

01:09 which can insert a certain interval between the retrial. So with this function, we can wrap our assertion in wait_for. And since wait_for returns a promise, we need to await it and also need to make our whole test callback asynchronous too. So now, if we try to rerun the tests,

01:29 we will see this notification test passing too. Because although at the first retry, this assertion will fail, it will complete on the next retry, or maybe the third retry, when this response body reading promise will resolve. You may be wondering, "Well, we're still awaiting something here.

01:46 Can't we just await something like a sleep function?" So let's implement one. A simple function that accepts a number and returns a promise that will resolve after this duration. So then we can call this sleep function here with the same 250 milliseconds and don't have to use the wait_for at all.

02:07 So let's give this a try. Hmm. The test still passes. So isn't this even better? It's less code compared to the implementation of this wait_for function. But here's the truth. This function seems smart, but tests are the worst place to be smart.

02:26 In fact, what we're doing here is introducing flakiness to our test. This is a sure recipe for failure. Because you see, the only thing that functions like sleep do is await a fixed time interval. In other words, we're trying to guess how long a certain side effect will take time. But we can't possibly know this. So we shouldn't be guessing our tests.

02:45 In comparison, when we're using a function like wait_for, it does use a certain timeout between the attempts it retries our callback. But this is only an implementation detail because we don't want to keep retrying forever. This certain side effect, it has to resolve, has to complete. But instead, it focuses not on the exact time,

03:03 but on the state that we want to achieve. And this is crucial to keep in mind. You should always rely on the state transition if you're dealing with a synchronicity that you can't control. Because that's the only thing that you know will happen and can reliably predict.