Testing a Service in Ionic 6

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.


Erik August Johnson is a software developer working with JavaScript to build useful things. Follow them on Twitter.