« Kevin Ngo

Mock Unit Testing a AngularJS Local Storage Service with Karma and Jasmine

21 Jan 2014

For a five-part introduction to AngularJS, check out my ng-okevin’s Angular.

I recently migrated my AngularJS to-do list app to AngularJS, and I wanted to unit test my Angular service that had a Local Storage schema migration. My app had a service that abstracted all interactions with Local Storage and implemented an interface to my list “model”. In this service, I modified the Local Storage schema, making it backwards-incompatible, so I wrote a migration. To make sure it worked, I unit-tested the entire service.


To unit-test Angular, I used:

Directory Structure

If you want to see for yourself my directory structure, check out the source code of my unit tests.

I keep a folder within my app called tests that makes room for both unit tests and end-to-end tests.

.
|-- tests
|   |-- e2e_tests
|   |   |-- conf.js
|   |   |-- minimalist_spec.js
|   |   `-- selenium
|   |       |-- chromedriver
|   |       |-- selenium-server-standalone-2.37.0.jar
|   |       `-- start
|   |-- node_modules
|   |   |-- karma
|   |-- package.json
|   |-- services.tests.js
|   `-- unit_tests
|       |-- karma.config.js
|       |-- lib
|       |   `-- angular-mocks.js
|       `-- services.tests.js
`--

Setting Up the Karma Test Runner

Install Karma.

npm install karma

Go into your unit test directory and initialize a configuration file. This will lead you through an interactive shell.

karma init karma.config.js

Look at your configuration file and make sure everything is correct. Things to double-check are basePath, files, frameworks, and browser. Here is my Karma config

Then we can start the Karma runner to run our tests.

karma start karma.config.js

Writing Jasmine Unit Tests

Jasmine unit tests, to me, are just like any other unit testing framework, but more designed to be read like English. In essence it is the same, there are test suites, test cases, setups, teardowns, and mocking. Here is my entire unit test for reference.

The Jasmine docs should be your primary source for learning what the tests look like, but here is an excerpt from my own.

describe('ItemService', function() {
    var store = {};
    var ls = function() {
        return JSON.parse(store.storage);
    };

    beforeEach(function() {
        // setUp.
        module('MinimalistApp');

        // LocalStorage mock.
        spyOn(localStorage, 'getItem').andCallFake(function(key) {
            return store[key];
        });
        Object.defineProperty(sessionStorage, "setItem", { writable: true });
        spyOn(localStorage, 'setItem').andCallFake(function(key, value) {
            store[key] = value;
        });
    });

    afterEach(function () {
        store = {};
    });

    it('migrate from legacy to version 0.', function() {
        store = {
            lastViewedList: 0,
            lists: ['sample', 'sample_two'],
            sample: {
                id: 0,
                list: [
                    {
                        id: 1,
                        items: ['item1', 'item2'],
                        rank: 2
                    },
                    {
                        id: 2,
                        items: ['item3', 'item4'],
                        rank: 1
                    }
                ]
            },
            sample_two: {
                id: 1,
                list: [
                    {
                        id: 1,
                        items: ['item5'],
                        rank: 1,
                    }
                ]
            },

        };
        localStorage.setItem('storage', JSON.stringify(store));

        inject(function(ItemService) {
            var sample = ItemService.getList(0);
            expect(sample.itemIndex.length, 2);
            expect(sample.items[0].text).toEqual('item3\nitem4');
            expect(sample.items[1].text).toEqual('item1\nitem2');

            sample = ItemService.getLists()[1];
            expect(sample.itemIndex.length, 1);
            expect(sample.items[0].text).toEqual('item5');
        });
    });
});

Initializing a Jasmine Test Suite

I’ll describe portions of the code starting from the top.

describe('ItemService', function() {
    // ...
});

Setup and Mocking LocalStorage

This initializes our test suite for our module.

var store = {};
beforeEach(function() {
    // setUp.
    module('MinimalistApp');

    // LocalStorage mock.
    spyOn(localStorage, 'getItem').andCallFake(function(key) {
        return store[key];
    });
    Object.defineProperty(sessionStorage, "setItem", { writable: true });
    spyOn(localStorage, 'setItem').andCallFake(function(key, value) {
        store[key] = value;
    });
});

The setup called before each test case for initialization. We mock out our app with Angular Mock’s module to allow us to inject, or import, the modules or pieces of our code that we wish to test.

Then we set up our Local Storage mock. we use Jasmine’s spyOn to mock localStorage.getItem and localStorage.setItem. This watches for calls to these methods, and instead of calling the code it normally runs, it’ll instead call the function that we pass into andCallFake. Here, the functions we pass into andCallFake simply interact with a plain Javascript object, store. We have mocked our Local Storage with a Javascript object, making infinitely easier to test. Everything is under our control. If you wish to mock Local Storage as well, definitely steal this code snippet.

afterEach(function () {
    store = {};
});

Jasmine Test Cases

The teardown called after each test case to reset the state. Here, we basically clear our Local Storage.

it('migrate from legacy to version 0.', function() {
    // ...
});

Injecting Angular Modules

A single test case. The test case is created with it where we pass in a string describing the behavior we are testing and the test case itself.

inject(function(ItemService) {
    var sample = ItemService.getList(0);
    expect(sample.itemIndex.length, 2);
    expect(sample.items[0].text).toEqual('item3\nitem4');
    expect(sample.items[1].text).toEqual('item1\nitem2');

    sample = ItemService.getLists()[1];
    expect(sample.itemIndex.length, 1);
    expect(sample.items[0].text).toEqual('item5');
});

After some test case setup code, we finally get to the juicy test assertions. We call Angular Mock’s inject to import our service into the test case. I assume the Angular app module namespace is searched to pull out the module so make sure the parameter name matches what we want to import.

Jasmine Assertions

To run a basic assertion, we call Jasmine’s expect, passing in the first value, and then toEqual to assert that its value is equivalent to what we expect. Jasmine is interesting how everything is modeled after being an English sentence.

That’s all. For additional resources, check out this guide and my app’s source code. I will soon be writing about how to write end-to-end (E2E) tests for Angular apps with Protractor.