Groking Angular: Dependency Injection and Testing
May 19th, 2014
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:
- Implicit dependencies – use function parameter names to indicate the dependencies. This method doesn’t work for minified code and should therefore be avoided.
$inject
– set$inject
property on the object which requires dependencies to an array of dependency names.- Inline array annotation – pass the list of dependencies as an array to the constructor function (like
factory()
orcontroller()
).
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:
module()
– used to bootstrap your app before every test.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?
[…] Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em http://tatiyants.com/groking-angular-dependency-injection-and-testing/ […]