I recently did a post about testing promises in an Angular application, and one about testing without using Angular's async zone.
This is yet another post about testing promises. In this post I want to use a spy to execute the tests instead of listening directly on the promise object.
The Sample
For simplicity, I'm going to use the same Angular application I created in the previous two posts. 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 }
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 spy: Spy;
One for the promise's resolveFunction and a second for the promise's rejectFunction. The final value is a reference to a spy. Now, add another beforeEach:
2 new Promise((resolve, reject) => {
3 resolveFunction = resolve;
4 rejectFunction = reject;
5 });
6 spy = 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, as is the actual spy.
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 resolveFunction(true);
5 spy.calls.mostRecent().returnValue.then((res: any) => {
6 expect(component.error).toBeTruthy();
7 done();
8 });
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, 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.
- Now we drill into the spy, get the return value from the most recent call--this should be the promise--and listen to it.
- 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 rejectFunction(false);
5 spy.calls.mostRecent().returnValue.then().catch(() => {
6 expect(component.error).toBeFalsy();
7 done();
8 });
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, 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.
- Now we drill into the spy, get the return value from the most recent call--this should be the promise--and listen to it.
- 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 this set of posts has been a lot of fun to explore. I hope you learned something from this. Go play with the code in the github for this blog.