When I first learned about testing Angular applications, I found it surprisingly difficult to understand. In particular, I kept getting hung up on the strange syntax required to set up test dependencies.

As it turns out, there is nothing especially hard about DI and testing in Angular. You just need to get used to the syntax. One way to do so is to pretend that it's something else. Allow me to explain.

Angular Testing The Easy (but Non-Existent) Way

Let's imagine that we're writing a Pastry Shop application. Our application needs a service for baking cakes, which we'll call Baker. The Baker service requires a few things to do its work. For example, it needs access to a Store where it can get the necessary ingredients. It also needs a Mixer and an Oven. In other words, it has certain dependencies.

Now, let's imagine that we're using a special version of JavaScript, which makes it easy to express dependencies using annotations. Here's what our Baker service might look like:

@inject(Store, Mixer, Oven)
function Baker() {
    var bake = function(item) {
        if (item === 'vanilla cake') {
            var ingredients = [ 
                Store.get('sugar'),
                Store.get('flower'),
                Store.get('eggs')
            ];
            var dough = Mixer.mix(ingredients);
            var cake = Oven.cook(dough);    
            cake.frosting = 'vanilla';
            return cake;         
        };
    };

    return: {
        bake: bake
    };
};

We're using an annotation called @inject to specify that Baker needs Store, Mixer, and Oven to do its work. This same annotation could be used in writing the corresponding tests:

@inject(Baker, Store, Mixer, Oven)
describe('Baker Service tests', function () {
    it('should bake vanilla cake', function () {
        // arrange
        spyOn(Store, 'get').andReturnValue({});
        spyOn(Mixer, 'mix').andReturnValue({});
        spyOn(Oven, 'cook').andReturnValue({});
        // act
        var cake = Baker.bake('cake');        
        // assert
        expect(Store.get).toHaveBeenCalledWith('sugar');
        expect(Store.get).toHaveBeenCalledWith('flower');
        expect(Store.get).toHaveBeenCalledWith('baking soda');
        expect(Store.get).toHaveBeenCalledWith('eggs');
        expect(Mixer.mix).toHaveBeenCalled();
        expect(Oven.cook).toHaveBeenCalled();
        expect(cake.frosting).toEqual('vanilla');
    });
});

Angular Testing the (less) Easy (but actually possible) Way

Sadly, the code above won't work in JavaScript. The good news is that Angular gives us multiple ways to do the same thing:

  1. Implicit dependencies – use function parameter names to indicate the dependencies. This method doesn't work for minified code and should therefore be avoided.
  2. $inject – set $inject property on the object which requires dependencies to an array of dependency names.
  3. Inline array annotation – pass the list of dependencies as an array to the constructor function (like factory() or controller()).

Of the three methods described above, #3 seems to be the most widely accepted. So, let's rewrite our code above using this method. First, let's look at the service:

// simple, but not possible
@inject(Store, Mixer, Oven)
function Baker() {...}

// noisy, but possible
angular.module('PastryShop').factory('Baker', ['Store', 'Mixer', 'Oven', 
    function(Store, Mixer, Oven) {...}
]);

Next, let's rewrite the test:

// simple, but not possible
@inject(Store, Mixer, Oven)
describe('Baker Service tests', function () {
    it('should bake vanilla cake', function () {...});
});

// noisy, but possible
describe('Baker Service tests', function () {
    var Baker, Store, Mixer, Oven;

    beforeEach(angular.mock.module('PastryShop'));

    beforeEach(function () {
        angular.mock.inject(function (_Baker_, _Store_, _Mixer_, _Oven_) {
            Baker = _Baker_;
            Store = _Store_;
            Mixer = _Mixer_;
            Oven = _Oven_;
        });
    });

    it('should bake vanilla cake', function () {...});
});

Let's understand what's going on here. Angular provides a special module called ngMock, which exposes (among other things) two very useful methods:

  1. module() – used to bootstrap your app before every test.
  2. inject() – used to get instances of various services injected into your tests. By convention, if you wrap the name of the service in underscores, Angular will strip those underscores before giving you the right service.

Once we obtain the necessary services from inject(), we simply store them within the scope of our describe() block and they become available to all of our tests.

A Note on Controllers

When testing controllers, there are a couple of extra things to consider. First, to actually create a controller for our tests, we need to use the $controller service. Second, since most controllers need a $scope to work with, we can get one by using $rootScope's $new() method.

Here's the code:

describe('Baker Controller tests', function () {
    var scope, BakerController, Baker;

    beforeEach(angular.mock.module('PastryShop'));

    beforeEach(function () {
        angular.mock.inject(function ($rootScope,  $controller, _Baker_) {
            scope = $rootScope.$new();
            Baker = _Baker_;

            BakerController = $controller('BakerController', {
                $scope: scope,
                Baker: Baker
            });
        });
    });
});

Once again, we're using the inject() method to get our dependencies, which in this case include $rootScope and $controller. We create an instance of a $scope and inject it, along with an instance of Baker, into the BakerController.

You may also like:

Did you love / hate / were unmoved by this post?
Then show your support / disgust / indifference by following me on Twitter!

This post got one comment so far. Care to add yours?

  1. […] Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em http://tatiyants.com/groking-angular-dependency-injection-and-testing/ […]