Angular Services with Karma and Jasmine Testing

viva las vegas - karma and jasmine go testing

Karma and Jasmine. Not the names of two girls I partied with in Vegas. Something even better.

Yes, let me introduce you to Jasmine testing, a Behaviour Driven Development testing framework. And her friend Karma, a spectacular test runner.

Now we are done with the introductions, let’s get down to detail. Testing an AngularJS / Ionic application is well documented and baked right in to the framework. That’s cool. Sometimes though, if your project reaches any sort of complexity above the tutorials, you are going to need to test Angular services that themselves have dependencies.

I found the documentation on this topic sparse and confusing. So hopefully this example can help shed a little more light on the subject.

The Situation

How can we unit test an Angular service that has another service (dependency) injected in to it?

Let’s say we have two services that we care about. Service A, and Service B.

Service A receives Service B via dependency injection.

How do we mock this up so that it becomes testable?

How can we fake the data coming back from Service B in a way that will allow us to make the dependency behave exactly how we want it to?

And how can we do this in such a way that our individual tests don’t become huge and unwieldy?

Before Each, Throw Me a Curve Ball

Assuming we have a service definition that looks like this:

“` language-javascript
‘use strict’;

angular.module(‘core’)

.service(‘TweetHoursStorage’, [‘storage’, function($localStorage) {
this.clearAll = function() {
$localStorage.clearAll();
};

// * snip *

}])
;
“`

Let’s say we have a test file that looks something like this:

(note: don’t copy / paste this, as it doesn’t work)

// app/modules/core/tests/services/tweet-hours-storage.service.test.js
'use strict';

describe('Service: TweetHoursStorage', function () {

    // load the service's module
    beforeEach(module('core'));

    var TweetHoursStorage;

    beforeEach(inject(function(_TweetHoursStorage_) {
        TweetHoursStorage = _TweetHoursStorage_;
    }));

    afterEach(function () {
        TweetHoursStorage.clearAll();
    });

    describe('update method test', function() {

        it('Tests the update method returns false if not passed an object', inject(function() {
            expect(TweetHoursStorage.update('blah')).toBeFalsy();
        }));

    // * snip *

When we run this, we will see an error similar to the following:

TypeError: queueableFn.fn.call is not a function

Why?

Well, it turns out there are two problems with the above code. One is more visible than the other, but we can fix both at the same time.

This issue occurs before we even hit our first test proper. This is a set up issue.

Firstly, beforeEach(module('core')); – this is redundant. We can remove this and combine with the second beforeEach(); method call just below.

Let’s do that:

// app/modules/core/tests/services/tweet-hours-storage.service.test.js
'use strict';

describe('Service: TweetHoursStorage', function () {

    var TweetHoursStorage,
        mockLocalStorage;

    // load the service's module
    beforeEach(function() {
        module('core', function($provide) {
            mockLocalStorage = jasmine.createSpyObj('mockLocalStorage', ['clearAll']);
            $provide.value('storage', mockLocalStorage);
        });

        inject(function(_TweetHoursStorage_) {
            TweetHoursStorage = _TweetHoursStorage_;
        });
    });

    afterEach(function () {
        TweetHoursStorage.clearAll();
    });

    describe('a clear all method test', function() {
        // * snip *
    });

Now, lines 11-14 go about creating a Jasmine spy object which allows us to pretend mockLocalStorage is really a real life, working as expected instance of $localStorage, which the service we are testing actually depends on – the line from our service definition above for reference:

.service('TweetHoursStorage', ['storage', function($localStorage) {

Once we implement this our tests should actually start running. They may / likely won’t be passing at this stage, but that bit is easier (in my opinion at least) than getting this set up the first time you do it.

Faking an API Call when Jasmine Testing

What about if we have a remote API that we need to get some data from?

How can we mock that out in a test?

 // app/modules/core/tests/services/selected-hours-to-notification-data-transformer.test.js

'use strict';

describe('Service: SelectedHoursToNotificationDataTransformer', function () {

    var SelectedHoursToNotificationDataTransformer,
        mockApiLookup,
        mockDateProcessing;


    // load the service's module
    beforeEach(function (){
        module('core', function($provide) {
            mockApiLookup = jasmine.createSpyObj('mockApiLookup', ['getAll']);
            mockApiLookup.getAll.and.callFake(function(){ return getFakeDataSet(); });
            mockDateProcessing = jasmine.createSpyObj('mockDateProcessing', ['process', 'nextStartDate']);

            $provide.value('ApiLookup', mockApiLookup);
            $provide.value('DateProcessing', mockDateProcessing);
        });

        inject(function(_SelectedHoursToNotificationDataTransformer_) {
            SelectedHoursToNotificationDataTransformer = _SelectedHoursToNotificationDataTransformer_;
        });
    });


    /**
     * This is the pretend result set that might be returned by a call to the API GET end point
     * @returns {*[]}
     */
    var start = moment().subtract(6, 'days');
    var getFakeDataSet = function () {
        return [
            { id: 4, title: 'event4', start: start, duration: 2 },
            { id: 5, title: 'event5', start: start, duration: 1 },
            { id: 8, title: 'event8', start: start, duration: 1 },
            { id: 99, title: 'event99', start: start, duration: 1 }
        ];
    };

This code may not be the most elegant or ninja-level JavaScript ever written, but by jingo it shows a few things that I would have found helpful a few months hence.

Line 15 – we are doing much the same as in the previous example.

This time though, we want to replace the real outcome with one we can control. Testing a remote API during our testing is not a good thing for many reasons including the API likely not being entirely tolerant to us repeatedly sending in the same fake data every time we run our Jasmine test suite. Also, speed, predictability, and many other factors.

First we declare the method as being available on our mock – line 15.

Then we tell Jasmine what we want anything that calls that method to get instead:

mockApiLookup.getAll.and.callFake(function(){ return getFakeDataSet(); });

Further down, starting on line 34, we do something that feels a little different to you if you’re used to working with PHP. We declare var getFakeDataSet which contains a yet to be executed function.

When that function is executed, it will return an array of JSON, representing the data we would anticipate receiving back from our remote API in the real world.

That data set will be entirely dependent on your project’s circumstances. My data is just for show.

So, back to line 16, where we return that data. The reason for me going into detail there is that it is entirely possible – and correct in some circumstances – that you would want to return the unresolved function.

mockApiLookup.getAll.and.callFake(function(){ return getFakeDataSet; });
// vs
mockApiLookup.getAll.and.callFake(function(){ return getFakeDataSet(); });

Just be careful of this one. It’s not specific to Jasmine testing, it’s a JavaScript thing in general, but it can be confusing.

I digress, but essentially the first line will return the function, unresolved until you call it by adding the () to the end of the variable holding that function. Confusing? A little. Tricky to spot for sure, when you inevitably make that mistake whilst coding.

Back to the example – line 19 and 20 show how to add multiple mocked dependencies should your service require it.

Jasmine Testing Cheat Sheet

The manual is pretty good on this topic.

But I also found this cheat sheet very helpful.

Published by

Code Review

CodeReviewVideos is a video training site helping software developers learn Symfony faster and easier.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.