A question came up on stack overflow recently about testing promises from an Angular application. In most cases, Angular applications do not use promises, instead opting for rxjs. But, I was curious enough to jump into this challenge. Some of my team mates have called me the "Unit Testing Whisperer".
The Sample
First, I had to make something that used promises. I created a new angular application with the Angular CLI. Then I created a new service named service-with-promise:
Inside the service, I created a method that used promises:
2
3@Injectable({
4 providedIn: 'root'
5})
6export class ServiceWithPromiseService {
7
8 constructor() { }
9
10 methodReturningPromise(): Promise<boolean> {
11 return new Promise((resolve, reject) => {
12 setTimeout(() => {
13 resolve(true);
14 }, 300);
15 });
16 }
17}
I'm using the promise, almost copied from the sample code over on Mozilla.
Now, open up the app.component.ts file. Inject the service:
And add a method to call the method that returns the promise:
2 this.serviceWithPromiseService.methodReturningPromise().then((result) => {
3 this.error = true;
4 }).catch((result) => {
5 this.error = false;
6 });
7 }
I also added an error variable so I can easily tell whether the error handler was executed or not:
You're more than welcome to run this application, but it doesn't do anything and I didn't add any code to trigger the method.
Write the Tests!
We want to test both the success and error handlers for this component. All testing starts with a describe block:
Inside the describe block, create a variable to hold a reference to the app component:
And now, add a beforeEach() to configure the TestBed:
2 await TestBed.configureTestingModule({
3 declarations: [
4 AppComponent
5 ],
6 }).compileComponents();
7 });
The TestBed is an Angular specific thing that sets up a mock Angular application for the purposes of testing.
Add another beforeEach() to set the component instance:
2 const fixture = TestBed.createComponent(AppComponent);
3 component = fixture.componentInstance;
4 });
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 two variables to the describe() block:
2let rejectFunction: (value: boolean | PromiseLike<boolean>) => void;
One for the promise's resolveFunction and a second for the promise's rejectFunction. Now, we're going to add another beforeEach:
2 spyOn(component.serviceWithPromiseService, 'methodReturningPromise').and.returnValue(new Promise((resolve, reject) => {
3 resolveFunction = resolve;
4 rejectFunction = reject;
5 }));
6 });
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 resolveFunction(true);
5 tick();
6 expect(component.error).toBeTruthy();
7}));
This is run in a fakeAsync() zone. This is an Angular specific zone, which gives us control over the internal browser timer. Here are the steps:
- 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.
- Then use the tick() function. this needs to occur to resolve the promise.
- Finally, check the value of the component.error property to make sure it was swapped from false to true
It took me longer to figure this out than I expected it to, but overall it worked great.
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 tick();
6 expect(component.error).toBeFalsy();
7}));
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.
- Then use the tick() function. this needs to occur to resolve the promise.
- Finally, check the value of the component.error property to make sure it was swapped from true to false
This was a fun sidebar and I hope you learned something from this. Go play with the code in the github for this blog.