5 February 2014

Unit Testing AngularJS

It's not always obvious how to write automated tests for the different components in AngularJS, so I'd like to share some of my techniques for testing AngularJS applications.

Automated Testing Stack

If you've got absolutely no automated testing setup at all, then I recommend looking at using one of the following to give you some scaffolding: angular-seed, Yeoman, or ng-boilerplate.

Here's a quick overview of each piece of the unit testing stack.

Karma: The Test Runner

Karma does the work of starting our browser(s), running the tests, and reporting the results in whatever format we desire.  It can also handle pre-processing code for doing things like compiling CoffeeScript or injecting Code Coverage markers.

Jasmine: The Test Framework

We need a format to write our tests in.  The default one used by the AngularJS community, and what I'll be writing the rest of this article with, is Jasmine.  It uses a BDD (Behaviour Driven Development) style, which essentially means it tries to make your tests read like business specifications that an analyst could understand.
Here's a quick example of a Jasmine test:


The "describe()" method is used to group tests.  The "it()" method is the specification: a descriptive string for the spec, and a function for testing that spec.  The "expect(value).toSomething()" is an assertion.  You pass a value to "expect()" and then you run what's called a 'matcher' method against it.  You can also run setup and teardown code using "beforeEach()" and "afterEach()" methods.
Karma has an adapter for interpreting the results from Jasmine, which it can then feed into various reporters.  So if Jasmine isn't your poison, chances are there's an adapter out there for whatever testing framework you prefer.

ng-mocks: The Helper Library

If you've ever downloaded AngularJS as an archive, you may have spotted the angular-mocks.js file.  This contains the ngMock module, which provides a set of helper functions to make your testing life easier.  Particular the inject() method which you can pass a function with injectable parameters (eg. Services), and it will handle all the dependency injection for you.

Services

Alright.  Down to the actual testing.
I'm going to start with the simplest example.  This will work for anything produced with "module.value()", "module.constant()", "module.factory()", or "module.service()".


The relevant parts for AngularJS developers is "module()" and "inject()".

The best way to think of the "module()" function is that it's doing the same job as the "ng-app" directive - it bootstraps that module so you can inject it's components.  The great thing is that the scope for that module only lasts for a single test.  So changes you make in one test won't have an effect on the next test.

The "inject()" method hooks into Angular's dependency injector.  So you can pass it a function with your dependencies, and it will handle their injection for you.  Although it won't work if the module those components belong to has not been loaded yet by "module()".

Here's a more verbose example; a pattern which I often use myself:


Handling Dependencies

I try not to make any function calls in my service constructor.  This makes things simpler if, for example, I depend upon another service which I want to mock during testing (It is called "unit" testing for a reason).  Lets say "myService.myMethod()" called "anotherService.anotherMethod()".  Rather than test what "anotherMethod()" does within the test for "myMethod()", I just want to confirm that it gets called.  I can do this by getting an instance of "anotherService", using "inject()", and replace "anotherMethod()" with a spy which tracks if and how it gets called:


If "myService" was using a method from "anotherService" in it's constructor, that would make things trickier, but not impossible, to test.  Services are constructed only when they're first injected (no point constructing something that's not even being used).  So the trick is to inject "anotherService" first, set up your spy, then inject "myService".


You couldn't do this if your constructor was calling one of it's own methods (eg. "myService.init()").  So when you're writing a complex constructor for a service, or controller, you really need to sit back and think "How am I going to test this?".

Filters

Filters are just functions.  The key to testing them is to get yourself a reference to those functions.  This is simple with "inject()".  You should set a parameter with the name of your filter with the suffix "Filter".  Here's an example:


Alternatively you can use the "$filter()" function, like so:


Controllers

Now we're getting somewhere interesting.  Controllers are different to services in that their dependencies aren't just services - they can be "resolve" values, such as "$scope".  On top of that, not all controllers just attach properties to "$scope".  Some will attach properties to themselves through "this".  2 examples are using controllers for directive to directive communication, and the new 'ng-controller="MyCtrl as scope"' optional syntax for controllers being introduced in AngularJS v1.2

So we need a way to inject specific dependencies into our controller, and then we (may) need a reference to the instance of that controller function.  The way to achieve this is to use the $controller service:


That works if you're not doing much with the $scope except for attaching properties.  But what if I'm using some of the built in functionality for scopes like "$watch", "$on()", "$broadcast()", or  "$emit()"?  You could create spies to mock all these things.  I personally like to use a real $scope object.  So how do I get one?  Inject "$rootScope" and call "$new()" on it:


Here's my template for controller tests:


Directives

The key to testing directives is to use the $compile service to compile a DOM element which includes your directive.  $compile will trigger your directive's code, and you can then start querying the DOM element and scope to test it's behaviour.
Here's a simplified version of the ng-hide directive (Similar to the original, but without $animate support):


To test it, we need to compile the directive, and then test how the element reacts when we change the scope or trigger use actions.
Here's a test taken straight from the AngularJS source code (The best source for writing and testing directives):


Walking through it, first it uses jqLite/jQuery to create a DOM element with the directive.
Then it passes the element to the $compile service, along with a scope object (in this case $rootScope), which runs the code for any directives it finds.
Then it tests that the element is still visible, since "exp" is undefined and therefore falsy.
Then it sets "exp" to "true", triggers the digest loop so that the $watch gets run, and then tests that the element is now hidden.

Most of your directive tests are going to follow this pattern some how:
  1. Create a DOM element with your directive.
  2. Pass it to $compile(), along with a scope object.
  3. Change the scope.
  4. Query the DOM for changes.

Providers

A provider is really no different from a service, except that it requires a special "$get()" method for "providing" the dependency, and it can exist during the "config" phase of AngularJS' lifecycle.
The trick is getting a reference to the provider in pristine condition (ie. Before "$get()" is called).  The way you do this is using the "module()" helper function, provided by ng-mocks:

In this scenario we have a provider called "myServiceProvider", which belongs to module "myModule".
We use the "module()" function to instantiate "myModule", and then get a reference to "myServiceProvider".
However, calling "module()" alone is not enough.  It doesn't actually do anything until "inject()" is called.  So we just call "inject()" with no dependencies, meaning "myServiceProvider.$get()" has still not been called.

Conclusion

AngularJS has been built from the ground up with testing in mind, but it's not always immediately obvious to new comers as to how they might test a particular component.
But once you know the trick, the pattern to adopt and the services to call, there's nothing to stop you writing a suite of tests you can rely on.
Then library upgrades become a trivial matter of: drop in the new version, run the tests, and fix any failures.  I couldn't use the weekly AngularJS builds without it.

No comments:

Post a Comment