I recently did a post about testing promises in an Angular application. That post explicitly used the Angular FakeAsync zone.
However, if at all possible I like to avoid special Angular creations when writing unit tests. How can I test a promise in a real async zone? I'm going to build off the Angular sample in the previous post and write tests using async Jasmine's tests.
The Sample
For simplicity, I'm going to use the same Angular application I created in the previous article. There is an Angular provider that has a method which returns a promise. The promise is resolved automatically on a timer.
Inside the app.component there is a method to call the service that returns the promise, like this:
2 this.serviceWithPromiseService.methodReturningPromise().then((result) => {
3 this.error = true;
4 }).catch((result) => {
5 this.error = false;
6 });
7 }
Our goal is to test this method using native Jasmine constructs without using Angular's fakeAsync zone.
Write the Tests!
The spec file creates the Angular framework with TestBed and uses that to get an instance of the component and service.
I'm going to add an embedded describe block for the method we're testing:
To run these tests we're going to want to spy on the function call that returns a promise, so we have complete control from the test when the result or error handlers are triggered. First, add three variables to the describe() block:
2let rejectFunction: (value: boolean | PromiseLike<boolean>) => void;
3let promise: Promise<boolean>;
One for the promise's resolveFunction and a second for the promise's rejectFunction. The final one will store a reference to the promise. Now, we're going to add another beforeEach:
2 promise = new Promise((resolve, reject) => {
3 resolveFunction = resolve;
4 rejectFunction = reject;
5 });
6 spyOn(component.serviceWithPromiseService, 'methodReturningPromise').and.returnValue(promise);
7});
This section of code uses a spy and returns a new promise. The resolve and rejection functions are saved to our local variables.
Interesting side note is that when dealing with Observables, the creation function is not run until something subscribes to the Observable. However, the Promise was created right away.
Now, let's write a test for the success method:
2 component.error = false;
3 component.callServiceWithPromise();
4 promise.then(() => {
5 expect(component.error).toBeTruthy();
6 done();
7 });
8 resolveFunction(true);
9});
This is run in a real async zone, which is represented using the done function parameter into the test function. Here are the steps the test runs:
- Set the component error property to false, because our intent is to make sure the component code swaps it to true.
- Then we call serviceWithPromise(). Now the component method should have added its custom result and error handlers to the promise.
- Next, we listen to the promise's result handler with the then function.
- Next, call the resolve function. This should cause the then() success handler inside the component to execute. It doesn't matter what value we pass in since it is not used in this code, but in a more real world application this value would mirror what your real world data you expect back from the promise.
- Finally, the then() result handler is inside the test. This checks that the component.error property was succesfully set to true and calls the done() function telling Jasmine that the test is complete.
Now let's write the method for testing the error handler. It is very similar:
2 component.error = true;
3 component.callServiceWithPromise();
4 promise.then().catch(() => {
5 expect(component.error).toBeFalsy();
6 done();
7 });
8 rejectFunction(false);
9});
Let's look at what this does:
- Set the component error property to true, because our intent is to make sure the component code swaps it to false.
- Then we call serviceWithPromise(). Now the component method should have added its custom result and error handlers to our spied upon promise.
- Next, we listen to the promise's result handler with the then function, and we chain the catch error handler after that.
- Next, call the reject function. This should cause the catch() error handler inside the component to execute. It doesn't matter what value we pass in since it is not used in this code, but in a more real world application this value would mirror what your real world data you expect back from the promise.
- Finally, the catch() error handler is run inside the test. This checks that the component.error property was succesfully set to true and calls the done() function telling Jasmine that the test is complete.
I use promises so rarely within Angular, but I hope you learned something from this. Go play with the code in the github for this blog.