A Tip for Angular Unit Tests with Promises

Mar 27, 2014 angularjs testing

tl;dr: Don’t put assertions inside promise callbacks.

Consider the following code:

The snippet above shows two ways of testing the return value of a promise. In the first method, the assertion occurs inside the promise resolution callback. In the second, we use a Jasmine spy to capture the return value, then assert on the spy. In both test cases, we resolve the promise with the value 10, then assert that it was resolved with the value 4. Obviously, the tests should fail, right?

If you actually run the tests, you’ll see that the first test case passes, while the second one fails. What’s going on? Looking at the second test case a little closer, we can see that it’s not failing for the reason we’d expect:

Error: Expected spy success to have been called with [ 4 ] but it was never called.

One thing that you’ll quickly run into when unit testing Angular applications is the need to hand-crank the digest cycle in certain situations (via scope.$apply() or scope.$digest()). Unfortunately, one of those situations is promise resolution, which is not very obvious to beginning Angular developers.

Let’s update the tests so that the promises are actually fulfilled:

Now we see the behavior we were expecting: both test cases fail, and they fail for the right reason. The key point here, though, is that only the second style detected an issue before we actually got the code right (which is one of the reasons we unit test!).

This example shows why I never put assertions inside promise callbacks: it’s too easy to create a false-negative test. Leaving off a scope.$digest() is a mistake I’ve seen every member on my team make, myself included. Even if you know better, sometimes that line just doesn’t make it into the code for whatever reason. Another benefit of the second approach is that the logic for the test flows from top-to-bottom - code is executed in the order that you read it. In the first approach, our assertion appears on the second line, but isn’t executed until the last line (if at all…).

Unit tests with promises aren’t the only place where you can run into trouble. This is just an example of a larger principle: minimize the possibility of false-negatives by keeping your assertions outside of conditional statements and non-linear control structures.

Thanks to Andy Fiedler for reviewing a draft of this post.