AngularJS and UI-Router testing — the right way, Part 3

Audrius Jakumavičius
Everon Engineering
Published in
3 min readOct 6, 2017

If you missed the first 2 parts of the series, take a look at unit testing UI-Router state configuration and transitions and unit testing AngularJS controllers. In this last part, we’ll look into testing AngularJS services.

There are two major ways of creating AngularJS services, using either a .service() or a .factory() method on a module, and you can find numerous arguments on the Net in favour of one or the other but, when it comes to simplicity and extensibility, nothing can beat the factory. Bellow we have a profileService factory with two methods: getProfile() and updateProfile().

Our getProfile() method makes use of sessionService to authenticate and return user data object from which we then extract a user profile info by calling sessionService.getUserProfile().

Since we want to test the service in isolation avoiding any side effects, we need to mock sessionService. That can be easily done using Angular’s $provide service. Here are the contents our profile.service.spec.js.

First, we create a mock sessionServiceMock that is a Jasmine spy object. It comes in handy because later we can manipulate return values from mocked sessionService methods.

var sessionServiceMock = jasmine.createSpyObj('sessionService', [
'authenticate',
'getUserProfile'
]);

Then we use $provide.factory() to replace our original sessionService with the mocked one.

$provide.factory('sessionService', function () {
return sessionServiceMock;
});

In the setUp function, we provide mocked responses to some API endpoints because we don’t want unexpected requests when $httpBackend.flush() is called.

function setUp() {
$httpBackend.whenGET(/.+(\.html|json)/).respond(200, {});
$httpBackend.whenGET('/api/profile').respond(200, {});
}

The getProfile() method test utilises previously created mock session service. Since sessionService.authenticate() is an async operation involving a roundtrip to the backend, it returns a promise. We need to emulate the same functionality in our test and this is where Jasmine’s spy object does the trick. We set up a return value for the method in a form of immediately resolved promise which is fulfilled with an authResponse object — $q.resolve(authResponse).

sessionServiceMock.authenticate.and.returnValue($q.resolve(authResponse));

The sessionService.getUserProfile() method is a simple function and we can manipulate its return value by replacing it with Jasmine’s fake function so when sessionService.getUserProfile() is called it returns auth.profile;.

sessionServiceMock.getUserProfile.and.callFake(function (auth) {
return auth.profile;
});

In the next few lines, we make a service call, set up expectations and make the necessary assertions. The key point here is $httpBackend.flush() — when called, it resolves any pending promises and, as a result, profileService.getProfile().then() handler is invoked, returning the profile object provided by a fake sessionServiceMock.getUserProfile().

profileService.getProfile()
.then(function onFulfilled(profile) {
expect(profile).toEqual(authResponse.profile);
});

$httpBackend.flush();

expect(sessionServiceMock.authenticate).toHaveBeenCalled();

The updateProfile() method test looks slightly different. Since we don’t depend on another service and call $http.patch directly, we can set up an expectation for the PATCH request to our API endpoint and provide a mocked request payload and response to it.

var profilePayload = {
name: 'Alex Stone'
};

$httpBackend.expectPATCH('/api/users/me', profilePayload).respond(200, profilePayload);

Then we call our service method with the same mocked payload, flush any pending requests which resolves the promise and fulfils the expectations.

profileService.updateProfile(profilePayload)
.then(function onFulfilled(profile) {
expect(profile).toEqual(profilePayload);
});

$httpBackend.flush();

Notice that in profileService.updateProfile(), we use $http.patch(). If you are not familiar with HTTP PATCH method, read up on it here — https://tools.ietf.org/html/rfc7396. By default, AngularJS doesn’t set correct header for $http.patch() requests but you can do this by injecting $httpProvider and setting Content-Type header on $httpProvider.defaults.headers.patch in your Angular application’s config block.

$httpProvider.defaults.headers.patch = {
'Content-Type': 'application/merge-patch+json'
};

This concludes our 3 part series on UI-Router and AngularJS unit testing but watch this space for more articles where we’ll explore some of the patterns and best practices of using UI-Router with AngularJS.

--

--

Audrius Jakumavičius
Everon Engineering

Senior front-end developer (freelance) at Royal Schiphol Group