Skip ahead!
š The test promise classš Our asynchronous codeš Returning a promiseš Resolving our promiseš Rejecting our promiseš Wrapping upThis article is a part of a series on unit testing in Angular. Some of the examples here might not make sense if you havenāt read the previous articles, so if you want to follow along with the whole thing, head to the first article!
Often when creating Angular applications, we will be dealing with some sort of asynchronous data, whether that be from a modal closing, one of your own asynchronous methods or awaiting a response from an external API. Luckily for us, Angular provides some great utilities for dealing with this. However, there are a few small utilities that we can use to make testing asynchronous code even easier. Without further ado, letās get started.
The test promise class
Conventially, promises cannot be resolved or rejected from outside of the promise. This makes it difficult for us to test, as we cannot control the state of the promise manually.
Luckily for us, thereās a way around this. Introducing, the test promiseā¦
test-promise.ts
export class TestPromise {
public promise;
public resolve;
public reject;
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
Pop this code in to a file of your choice, preferrably inside a utilities
folder. By assigning the resolve
and reject
callback to properties of the test promise class, weāre now able to resolve them from outside of the promise. Woohoo!
Our asynchronous code
Say in our component, we have a function for fetching our products asynchronously:
product-list.component.ts
ngOnInit(): void {
this.getAllProducts();
}
public async getAllProducts(): Promise<void> {
const allProducts: Array<IProduct> = await this._productService.getAllAsync();
this.products = allProducts;
}
We need to first add the getAllAsync
to our stubbed service. If youāre unsure, you can refer back to the stubbing dependencies article to see how.
Once weāve done that, we need to create our test promise, and ensure that the call to this service returns that promise.
Returning a promise
Within our initialisation describe
block, we can do just that:
product-list.component.spec.ts
describe('on initialisation', () => {
let getProductsPromise: TestPromise;
beforeEach(() => {
getProductsPromise = new TestPromise();
(dependencies.productService.getAllAsync as jasmine.Spy).and.returnValue(
getProductsPromise.promise
);
fixture.detectChanges();
});
it('should fetch all of the products', () => {
expect(dependencies.productService.getAllAsync).toHaveBeenCalledWith();
});
});
Now, when this service is called on initialisation, it will return the promise. Our test suite will now be paused, as it is in a state of awaiting
the promise. To make the application continue, we need to resolve
this promise with our data.
To do this, weāre going to use a nice little utility called fakeAsync
. This acts in a similar way to the async
method, but it allows us to pass time in the application at our own speed. Letās take a lookā¦
The async method is used when resolving promises inside a
beforeEach
block to let Angular know that we are testing asynchronous code.
Resolving our promise
Underneath our test for fetching the products, we have:
product-list.component.spec.ts
describe('when the products have been fetched', () => {
beforeEach(fakeAsync(() => {
getProductsPromise.resolve([{ name: 'product', number: '1' }]);
tick();
fixture.detectChanges();
}));
it('should display the products', () => {
expect(getProducts()[0].componentInstance.product).toEqual({
name: 'product',
number: '1'
});
});
});
As you can see in our beforeEach
block, we resolve our promise with the array that we want it to return. We then call Angularās tick
method in order to ālock inā the changes, which we can then apply using fixture.detectChanges()
.
With that, our asynchronous call is now resolved and we can test to see if the products are properly displayed!
Rejecting our promise
Often in our code, we will be catching our errors in order to handle them correctly. Letās extend our asynchronous code to do just that.
product-list.component.ts
public async getAllProducts(): Promise<void> {
try {
const allProducts: Array<IProduct> = await this._productService.getAllAsync();
this.products = allProducts;
} catch (e) {
console.log(e);
}
}
In the real world, we would be doing something a bit more sophisticated than this. However, this is enough for us to show how to deal with promise rejections in our test file.
Just after our describe block stating when the products have been fetched
, we can cover the reject case with when something goes wrong when fetching the products
.
product-list.component.spec.ts
describe('when something goes wrong when fetching the products', () => {
beforeEach(async(() => {
spyOn(console, 'log');
getProductsPromise.reject('error!');
}));
});
In our beforeEach
block, we make it async
(we donāt need fakeAsync
this time or detectChanges
this time because we are not testing the template). Before we reject our promise, we need to spy on the console.log
method. Luckily for us, we can spy on static methods using spyOn
.
spyOn
is a testing method provided by Jasmine that allows us to pass in an object and a method name that we want to spy on. Whenever this method is then called, it will then call a spy in its place.
After weāve spied on our method, we reject our promise with an error.
getProductsPromise.reject('error!');
This will make our asynchronous call throw an error, which will be caught in our catch
block.
} catch (e) {
console.log(e);
}
Finally, just after our beforeEach
block, we can make sure that console.log
gets called with the error that gets thrown. and with that, we now have 3 passing tests!
product-list.component.spec.ts
it('should log the error', () => {
expect(console.log).toHaveBeenCalledWith('error!');
});
Wrapping up
In this article weā ve learned how to return a promise from an external method, resolve that promise, and also how to reject that promise. Next up, weāll be discussing how to mock observables.