Asynchronous Unit Tests With Mocha, Promises, And WinJS
Before I get in to the guts of this post, you need to read Christopher Bennage’s post on how we have our Mocha test suite set up for our Win8 / WinJS project. It’s not the best setup, but it works. It allows us to write tests for our app, even if it is a little bit painful to constantly link files in to the test project. I have hopes that we’ll find a better solution for this, but for now, we’re moving forward with this because it works.
I’ve already talked about how you need to understand promises in order to build Win8 / WinJS applications. It’s an absolute must because there is almost no way you can build an app without using them. Anything you might build that doesn’t use them would likely be very trivial and not very useful, honestly. They are baked in to the WinJS framework very deeply and I see this as a good thing. But understanding how to use a promise is very different than understanding how to write tests for code that uses promises.
Asynchronous Mocha Tests
Testing a promise means that you’re testing asynchronous code. You have no knowledge of when the asynchronous code will finish. Because of this you can’t just assign some value to a variable in the async callback and hope your test will catch it. This can be demonstrated easily by using a call to “setTimeout” within a Mocha test, using Chai as the expect framework:
Now you might think that this test should pass. When the “expect” is run inside of the specification, it should verify that the “foo” variable is set to true because the code in the setTimeout call sets it to true. But the test fails because the expectation is run before the callback in the setTimeout is called.
To correct this, Mocha can provide a “done” callback method that you can call from the beforeEach call, afterEach call, it calls, and more. This “done” parameter, when present in your callback function, tells Mocha that you are writing an asynchronous test. This causes mocha to enter a timer when the function with the “done” parameter runs, waiting for the async function to finish – which is facilitated by either calling the `done()` function, or by exceeding a 2 second timeout.
The basic idea behind the `done()` call is that you call this after your async code has completed, and your test has modified everything it needs to modify, so you can check the results correctly.
With that in mind, we can update our test to use the done call:
And now the test passes. We’re telling Mocha to wait for the async code to complete before it runs the expectations. When the timeout finally fires, we set our “foo” variable to true and then call `done()`. This tells Mocha that the async test is finished and it moves on to the expectations.
Testing The Value In A Promise’ Callback
When a promise resolves, it often provides a value as a parameter to the callback function. This allows you to get the information that you requested in an asynchronous manner. And it’s often the case that the value of this async callback needs to have assertions or expectations set against it. There are a couple of ways that this can be done, depending on your specific scenario and the code you’re trying to test.
The first way is to add your “then” callback in the beforeEach of the test. You would assign the value from this function to a variable that is set up in the “describe” function. Then your expectations can check the value.
This works well for very simple scenarios and I generally recommend doing this when you can. It’s the easiest way to test the result of the promise. However, that’s not always an option. Sometimes a test is more complicated, has more expectations that it needs to verify, etc. In more complex situations you might find that the number of variables you have to create in the describe block is getting out of hand, or that the beforeEach function is doing too much work – work that should legitimately be in the expectations. In this scenario, you can move the async test and the “done” call in to the expecation’s “it” function. But it comes with a small trick that you need to do.
Expectations In Promises Are Problematic
Moving the async “done” call to an expectation is easy. You just add the “done” parameter to the “it” function and call it – the same as you would do in the beforeEach function.
But testing a WinJS promise with Mocha introduces a small complexity. When you call the “expect” function and the expectation fails, an exception is thrown. Normally, Mocha intercepts this exception from the expectation and reports it as a test failure. This is how Mocha works and it works well… until we introduce WinJS promises.
The problem with promises is that they intercept the expectation’s error throwing for us, preventing Mocha from seeing the error. For example, in this test, we should see a message saying it expected “false” to be “true”. What we get instead is a timeout error saying the test exceeded 2 seconds (2000ms).
When the promise completes, a value of false is passed in as the parameter of the “then” function. The expectation throws an error because it expects false to be true. This is all good so far. We expect this to fail. The problem comes in when the exception is thrown, though. Normally, the exception would be caught by the Mocha test runner which would cause the test to fail and Mocha would report the failure. But in this case, the promise is catching the error for us. That means the error never makes it out to Mocha. Throwing an error also means the “done()” function is never called, so the test goes in to a wait state and Mocha times out at 2 seconds, giving us a report of the testing timing out instead of the message from the failed expectation.
Done, Done, Done, Done: Async Expectations With WinJS Promises
To fix this problem, we need to take advantage of how WinJS promises work. According to the WinJS promises documentation and the Promises/A Spec that WinJS promises implements, throwing an exception in a “then” function causes the error to be forwarded to the error handling function of the promises “done” function. In other words, we can get access to the error that the expectation threw by chaining a “done” call on to the end of the “then” call:
Ok, this is going to be a bit confusing. The “promise.then().done()” call is the promise’s “done” function. But Mocha also provides a “done” callback to the beforeEach and it functions… So there’s a lot of “done” in the upcoming discussion. I’ll try to keep things clear as much as I can.
At this point, we need to complete the async expectation. But we don’t want to call Mocha’s “done()” callback as-is because that would tell Mocha that the test passed. Instead, we need to call Mocha’s “done” callback with a parameter – the error that we caught. Passing a parameter to the Mocha’s “done” function – any parameter – tells Mocha that the test failed. It assumes that the parameter you pass in is the error from the expectation or the reason that the test failed. That means we can call Mocha’s “done” function with the “error” parameter of our error handling callback:
Note the “null” first parameter of the promise’s “done” call. This is there because the “done” method takes 3 parameters as noted in the previous code snippet.
And we can make the code even more simple than this. Since the error handling callback of the promise has the same method signature of Mocha’s “done” callback, we can get rid of the inline function and pass “done” as the callback directly:
Lastly, we still need to call Mocha’s “done()” from within our promise’s “then” function. This tells Mocha that the test is complete in cases where the expectation is met (the test passes).
Now you might be tempted to pass Mocha’s “done” as a parameter to the promise’s “then” and “error” callbacks of the promise’s “done”, like this:
That won’t work, though, because the “then” callback receives a parameter from the promise and this would forward the parameter to Mocha’s “done” function, which would be interpreted as a test failure. So we have to keep Mocha’s “done()” call for a successful test in an inline callback of the promise’s “then” function, with our expectation.
With this in place our test passes and fails correctly. We get the error message that says it expected false to be true when we complete the promise with a value of “false”, and we get a passing test when we complete the promise with a value of “true”.
(… yeah, that’s a lot of “done” calls in our expectation, but it works. Hopefully that made sense.)
Async Tests: Problem Solved