Tuesday, April 28, 2015

I’m Not Mocking You, Just Your AngularJS Tests

You could alternatively call this post, “Avoid redundancy in your Angular unit tests.”

There are myriad approaches to implementing service calls in Angular. Some developers use the $resource service for pure REST endpoints. I prefer to isolate web interactions in a component of their own that returns a promise. Although $http returns a promise, you have to know how to parse the pieces, such as looking at the data property instead of the result itself, so I prefer to wrap it.

A service in my apps ends up looking something like this:

var blogExample;
(function (blogExample) {
 
    "use strict";
    var app = blogModule.getModule();
    function service(baseUrl, $http, $q) {
        this.url = baseUrl + "api/examples";
        this.$q = $q;
        this.$http = $http;
    }
    angular.extend(service.prototype, {
        getStuff: function() {
            var defer = this.$q.defer();
            this.$http.get(this.url).then(function(result) {
                defer.resolve(result.data);
            }, function(err) {
                defer.reject(err);
            });
            return defer.promise;
        }
    });
    app.service("blogExampleSvc", ["baseUrl", "$http", "$q", service]); })(blogExample || (blogExample = {}));

If you are using TypeScript you can further give it an interface, like this:

module BlogExample {
    interface IExampleService {
        getStuff: () => ng.IPromise<any>;
    } } 

Now if you’ve been working with Angular for any time you know you can write tests by pulling in ngMock and setting up the $httpBackend service. I might test that the service responds correctly to a successful request like this (using Jasmine):

(function () {
    "use strict";
    var exampleService,
        httpBackend;
    describe("exampleService", function () {
        beforeEach(function () {
            module("blogExample", function ($provide) {
                $provide.constant("baseUrl", http://unittest/);
            });
        });
        beforeEach(inject(function ($httpBackend, blogExampleSvc) {
            httpBackend = $httpBackend;
            exampleService = blogExampleSvc;
        }));
        afterEach(function () {
            httpBackend.verifyNoOutstandingExpectation();
            httpBackend.verifyNoOutstandingRequest();
        });
        it("is registered with the module.", function () {
            expect(exampleService).not.toBeNull();
        });
        it("should set the proper URL for the service", function() {
            expect(exampleService.url).toBe(http://unittest/api/examples);
        });
        describe("getStuff",function () {
            it("should return stuff upon successful call", function () {
                exampleService.getStuff()
                    .then(function (result) {
                        expect(result).not.toBeNull();
                    }, function () {
                        expect(false).toBe(true);
                    });
                httpBackend.expectGET(exampleService.url).respond(200, []);
                httpBackend.flush();
            });
        });
    }); })();

(You’ll want to check for more than just not null on the call, but I’m trying to keep it simple.)

Inevitably that service will get pulled into a controller. Ironically, this is when the dynamic nature of JavaScript makes it easier to test things, but I still see people configuring the HTTP (test, mock) backend for their controller tests! Why?

Remember, unit tests should be simple, fast, and easy to write. When you test the service, you are test that the service does it’s job. It should make the calls it needs to make and deal with the response codes. When you test the controller, you should assume the service has passed its tests. You should only be concerned about how the controller interacts with the service, not how the service interacts with the backend. That’s redundant!

So in order to deal with a controller that depends on your service, you should mock the service.

There are several ways to do this. The simplest is to just create your own mock on the fly. JavaScript is really, really good at this. Here’s an example where I create the mock directly and use it to count calls. Notice that in the module configuration, I override the “real” service definition with my mock one, and then use the injector to wire up the promise service. I also define a handy function named flushPromises that triggers digest so promises are satisfied.

(function() {
    "use strict";
    var controllerSvc,
        exampleController,
        exampleSvcMock,
        flushPromises;
    describe("exampleCtrl", function() {
        beforeEach(function() {
            exampleSvcMock = {
                count: 0,
                $q: null,
                getStuff: function() {
                    var defer = this.$q.defer();
                    defer.resolve([]);
                    this.count += 1;
                    return defer.promise;
                }
            }
            module("blogExample", function($provide) {
                $provide.constant("baseUrl", "http://unittest/");
                $provide.value("blogExampleSvc", exampleSvcMock);
            });
        });
        beforeEach(inject(function($controller, $q, $rootScope) {
            exampleSvcMock.$q = $q;
            controllerSvc = $controller;
            flushPromises = function() {
                $rootScope.$apply();
            };
        }));
    }); });

If I were using TypeScript I would define my mock as the IExampleService type to ensure I am satisfying any requirements of the contract. Now, instead of setting backend expectations, I simply query my mock instead:

describe("refresh", function () {
    it("calls getStuff", function () {
        var count;
        exampleController = controllerSvc("exampleCtrl");
        count = exampleSvcMock.count;
        exampleController.refresh();
        flushPromises();
        expect(exampleSvcMock.count).toBeGreaterThan(count);
    }); });

It’s as simple as that! Now I’m mocking my tests instead of them mocking me. If you have a more complex component to mock, you can use built-in helpers. For example, Jasmine ships with its own spies for this purpose.

The bottom line is to avoid complexity (when a component calls a component calls a component) also isolate your components so they each have one responsibility, then mock your dependencies. You may even grow to enjoy testing more!

What are your thoughts on testing?

signature[1]

1 comment:

  1. Hi Jeremy!
    Is there a reason that:
    getStuff: function() {
    var defer = this.$q.defer();
    this.$http.get(this.url).then(function(result) {
    defer.resolve(result.data);
    }, function(err) {
    defer.reject(err);
    });
    return defer.promise;
    }
    Can't be written as:
    getStuff: function() {
    return this.$http.get(this.url)
    .then(function(result) {
    return result.data;
    });
    }
    ?

    RE mocking of dependencies in the controller, I've found that the extra amount of code required for setting up stub promises and flushing of them by calling digest etc can increase the complexity of the test over just using http backend and the real components. I try to keep my controllers so simple that not a lot happens in them apart from setting properties and calling off to other services to do the "real" work. They don't actually have much logic to put under test as they are really just co-ordinators of other services at the end of the day.

    The controller tests when written with real dependencies seem to work fine most of the time - I actually find it a useful test to assert that the button click function in the controller does actually make a call to this url with these parameters (i.e using http backend), testing that all the layers are hooked up correctly in order for that to happen. The problem I find with unit testing a co-ordinator in isolation (such as a controller with no real logic in it), can be the tests can just become a line by line assertions to lines in the co-ordinator and hence coupled to it's implementation.

    Drilling down and putting a particular service under test that has real algorithmic value can be where the logic of a complex service is tested rather than all in the controller test (so you still don't get a over complex controller test by including the real dependencies and attempting to actually test all the dependencies properly in it as well).

    Including real dependencies in the controller test can be a good opportunity to test the overall behaviour of the controller, and decouples your test from the implementation so that you are more free to refactor your code without actually changing your tests, and watching them stay green - rather than continually be given inertia by your tests by breaking them all every time you need to refactor.

    A lot of this thinking comes from Ian Cooper's talk here https://vimeo.com/68375232 - highly recommend watching as it changed how I thought about it all myself after experiencing pain with tests that are too tightly coupled to implementation details rather than behaviour.

    Of course there are still things I need to stub out in the controller tests, however a lot of the time you can get away with using the real components with the benefit of actually keeping the test simpler, including a few more units under test (could be argued as a good thing for overall confidence in the system), and decoupling the tests away from implementation details (more BDD behaviour of the controller function overall rather than just what the controller itself does). It's trying to decide what the 'unit' is you are testing.

    Some controllers I have to stub some dependencies but not all - sometimes I only have to do this because of a general design issue.

    I totally get mocking, have done it plenty in the past and still do, however I'm starting myself to experiment with reducing the amount of it (after watching the video linked above by Ian Cooper)! I feel I end up with simpler more integrated tests that need to change less often for the refactorings that typically take place in a system.

    Hamish

    ReplyDelete