Introduction
Previously, we looked at testing a page in Ionic. Now, we will take a look at how to test a service, another important place to add test coverage.
Setup
The initial set up is similar, as we will be creating a .spec.ts
file for a given service, in this case, birthday.service.spec.ts
.
The first thing we will do is create our initial describe
:
describe('BirthdayService') => {
});
Next, as we did with testing a page, we will using TestBed:
import { TestBed } from '@angular/core/testing';
describe('BirthdayService') => {
TestBed.configureTestingModule({
});
});
Now, let’s have a look at BirthdayService
in order to get an idea of what dependencies it uses:
constructor(private http: HttpClient, private toastController: ToastController) {
this.getFromServer();
}
Looks like we have two dependencies injected, HttpClient
and ToastController
. Let’s start with HttpClient
:
Angular provides us with a module dedicated to testing the HttpClient
:
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
First, we import the module to our TestBed configuration:
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: []
});
Next, we will inject HttpTestingController
:
let httpTestingController: HttpTestingController;
httpTestingController = TestBed.inject(HttpTestingController);
Now, our test looks like:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('BirthdayService', () => {
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
]
});
httpTestingController = TestBed.inject(HttpTestingController);
});
});
Now let’s add the service under test, BirthdayService
and a mock of the other dependency, ToastController
:
import { TestBed } from '@angular/core/testing';
import { BirthdayService } from './birthday.service';
import { ToastController } from '@ionic/angular';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('BirthdayService', () => {
let httpTestingController: HttpTestingController;
let mockToastController;
let service: BirthdayService;
beforeEach(() => {
const toast = jasmine.createSpyObj('Toast', [
'present',
]);
mockToastController = jasmine.createSpyObj('ToastController', ['create']);
toast.present.and.returnValue(Promise.resolve());
mockToastController.create.and.returnValue(toast);
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
BirthdayService,
{
provide: ToastController, useValue: mockToastController
}
]
});
httpTestingController = TestBed.inject(HttpTestingController);
service = TestBed.inject(BirthdayService);
});
});
As you can see, we mock the ToastController
for the create
method.
The inject
function is then called on BirthdayService
giving us a handle on the service. With setup complete, the next thing to do is write some tests!
Writing Tests
The first function we want to get coverage on is the one called in the constructor of the BirthdayService
: getFromServer
.
Let’s add a describe block for the function and begin by checking if the correct URL is being called:
describe('getFromServer', () => {
it('should call GET with correct URL', () => {
// Test that the URL was correct
const req = httpTestingController.expectOne('http://localhost:3000/birthdays');
req.flush([]);
expect(req.request.url).toBe('http://localhost:3000/birthdays');
expect(req.request.method).toBe('GET');
httpTestingController.verify();
});
it('should return an empty array', () => {
// From flush:
expect(service.birthdays).toEqual([]);
});
});
In this test, we do not have to manually invoke the getFromServer
method because it is being invoked in the constructor
of the service.
As you can see, we have an expectOne
method being invoked from httpTestingController
. Additionally, a couple expectations to make the test runner happy, otherwise it would complain we have no expectations in this test. There is finally a test that the birthdays
property is an empty array (this comes from the flush
method).
Now, let’s test our method with the ToastController
dependency:
describe('getErrorToast', () => {
it('should call create method', () => {
service.getErrorToast();
expect(mockToastController.create).toHaveBeenCalled();
});
});
And that was simple enough. This service has several more methods but they are all similar to getFromServer
as they mainly make HTTP calls.
One more function we will test, getBirthdays
which returns an observable:
describe('getBirthdays', () => {
it('should return an empty array', () => {
service.getBirthdays().subscribe(birthdays => {
expect(birthdays).toEqual([]);
});
});
Conclusion
As you can see, much like testing a page in Ionic, testing services requires a fair amount of setup. Luckily, what we setup is pretty good - the HttpClientTestingModule
for one and of course, TestBed
. Using these tools as well as mocks, we can go pretty far to get good coverage of our services.