The Controller: What are you testing?
First off, what is a controller? Well, in Angular all a controller really does, for the most part, is accept an injected $scope object and alter it by adding properties, member functions, and calling methods on it (such as $apply and $watch). That's really it. For those of us coming from other MVC backgrounds in other languages, it might feel like a class declaration of some sort with methods defined in the actual controller, but in reality, it's just a function that gets called to set up the state of a $scope object that is passed to it. Sometimes there are services injected into it that also get called or get their state altered, but I'll get to that in a bit.
Recommended Testing Suite: Jasmine
The recommended tool for testing Angular is Jasmine. You can, of course, use any unit testing tool you like, but for this blog entry, we'll be using Jasmine. To get started with Jasmine they have some really well annotated code on their site as a tutorial of sorts, but I'd recommend just going to Plunker and starting a new "Angular + Jasmine" Plunk and fiddling around until you get the hang of it.
To TDD or not to TDD? Yes.
I'm not going to go into the specifics of TDD, whether or not you should use it, the pros and cons of TDD, or even attack this blog entry from that angle. I'm going to assume that if you're here, you've probably written some Angular controller, or you know how to write an Angular controller, and you're thinking "how do I test this thing?". So we'll just cover some of those basics, mkay?
The Basic Setup
Let's start off with the basic Jasmine Set up. This is what's required to run the Jasmine specs you're going to write, and produce a report in HTML format you can read. So to do all of this, you will create some HTML file, we'll call it "SpecRunner.html" and this would be the basic content of it:
<!doctype html>
<html>
<head>
<!-- jasmine -->
<script src="jasmine.js"></script>
<!-- jasmine's html reporting code and css -->
<script src="jasmine-html.js"></script>
<link href="jasmine.css" rel="stylesheet">
<!-- angular itself -->
<script src="angular.js"></script>
<!-- angular's testing helpers -->
<script src="angular-mocks.js"></script>
<!-- your angular app code -->
<script src="app.js"></script>
<!-- your Specs (tests) -->
<script src="specs.js"></script>
</head>
<body>
<!-- bootstrap jasmine! -->
<script>
var jasmineEnv = jasmine.getEnv();
// Tell it to add an Html Reporter
// this will add detailed HTML-formatted results
// for each spec ran.
jasmineEnv.addReporter(new jasmine.HtmlReporter());
// Execute the tests!
jasmineEnv.execute();
</script>
</body>
</html>
An Example Controller
So now we'll need something to test. I'm going to make up a completely contrived controller to create some unit testing examples against. Nothing special, and nothing that might even make sense. It's just different things you might commonly do in an Angular controller, that you might need to test. In the specsRunner.html file above, this would be our "app.js".
var app = angular.module('myApp', []);
/* Set up a simple controller with a few
* examples of common actions a controller function
* might set up on a $scope. */
app.controller('MainCtrl', function($scope, someService) {
//set some properties
$scope.foo = 'foo';
$scope.bar = 'bar';
//add a simple function.
$scope.test1 = function (){
$scope.foo = $scope.foo + '!!!';
};
//set up a $watch.
$scope.$watch('bar', function (v){
$scope.baz = v + 'baz';
});
//make a call to an injected service.
$scope.test2 = function (){
//an async call returning a promise that
//inevitably returns a value to a property.
$scope.fizz = someService.someAsyncCall($scope.foo);
};
});
/* Simple service example.
* This is a service created just to use as an example of
* some simple service that is making some asynchronous call.
* A real-life example of something like this would be a
* service that is making $http or $resource calls, perhaps. */
app.factory('someService', function ($timeout, $q){
return {
// simple method to do something asynchronously.
someAsyncCall: function (x){
var deferred = $q.defer();
$timeout(function (){
deferred.resolve(x + '_async');
}, 100);
return deferred.promise;
}
};
});
Unit Tests For Our Example Controller
So, given the above controller, here is a battery of unit tests that tests the behavior of this controller. Well, more importantly, it tests what has been set up on the $scope by the controller function. In the specsRunner html (above), this would be in our "specs.js":
describe('Testing a controller', function() {
var $scope = null;
var ctrl = null;
/* A mocked version of our service, someService
* we're mocking this so we have total control and we're
* testing this in isolation from any calls it might
* be making.
*/
var mockService = {
someAsyncCall: function (x){
return 'weee';
}
}
//you need to indicate your module in a test
beforeEach(module('myApp'));
/* IMPORTANT!
* this is where we're setting up the $scope and
* calling the controller function on it, injecting
* all the important bits, like our mockService */
beforeEach(inject(function($rootScope, $controller) {
//create a scope object for us to use.
$scope = $rootScope.$new();
//now run that scope through the controller function,
//injecting any services or other injectables we need.
ctrl = $controller('MainCtrl', {
$scope: $scope,
someService: mockService
});
}));
/* Test 1: The simplest of the simple.
* here we're going to test that some things were
* populated when the controller function whas evaluated. */
it('should start with foo and bar populated', function() {
//just assert. $scope was set up in beforeEach() (above)
expect($scope.foo).toEqual('foo');
expect($scope.bar).toEqual('bar');
});
/* Test 2: Still simple.
* Now let's test a simple function call. */
it('should add !!! to foo when test1() is called', function (){
//set up.
$scope.foo = 'x';
//make the call.
$scope.test1();
//assert
expect($scope.foo).toEqual('x!!!');
});
/* Test 3: Testing a $watch()
* The important thing here is to call $apply()
* and THEN test the value it's supposed to update. */
it('should update baz when bar is changed', function (){
//change bar
$scope.bar = 'test';
//$apply the change to trigger the $watch.
$scope.$apply();
//assert
expect($scope.baz).toEqual('testbaz');
});
/* Test 4a: Testing an asynchronous service call.
* Here we don't really even need to worry about asynchronicity.
* Since we make a mockService (above) and injected it,
* We can take out the asynchronous stuff and just test that it's
* returning a value. We can test the service's asynchronous behavior
* when we test the service, it's not the controller's concern. */
it('should update fizz asynchronously when test2() is called', function (){
//just make the call
$scope.test2();
//assert
expect($scope.fizz).toEqual('weee');
});
/* Test 4b: Probably should test that the service method was
* called as well. We'll use Jasmine's spyOn() method to do
* this. */
it('should make a call to someService.someAsyncCall() in test()', function (){
//set up the spy.
spyOn(mockService, 'someAsyncCall').andCallThrough();
//make the call!
$scope.test2();
//assert!
expect(mockService.someAsyncCall).toHaveBeenCalled();
});
});
The Simple Tests
I don't want to dwell too much on the first two tests. They're fairly straight forward, and I don't want to patronize anyone that's made it this far. They're your basic, basic, unit tests. Make a call, assert a value, the end.
Testing a $watch()
Okay, here there's a little trick. If you have a $watch set up on a property, or on anything really, and you want to test it, all you need to do is update whatever you're watching on the $scope (or wherever it is), then call $scope.$apply(). Calling $apply will force a digest which will process all of your $watches.
Testing Service Calls and Asynchronous Service Calls
Really, anything asynchronous you do in an Angular controller should be done in some sort of injectable service. Whether that injectable service is $timeout, or some service you created yourself that makes async calls with $http or $resource. Because we're testing the controller, and because the service is injectable, you really shouldn't have to worry about the asynchronous nature of what's happening inside the service. Testing that should be done in your service unit tests, not your controller unit tests. So what we're going to do here is create a mock service (mockService in the example above). Because we're doing this we have a lot of control. Test 4a above shows a test that tests the result of the call. Test 4b above shows a tests that uses Jasmine's spyOn() method to verify that our service method was indeed called.
View the complete example on Plunker
More to come...
I realize this is an article on unit testing just one, small, relatively easy to test part of Angular. And there are much scarier things to right tests for... like say... directives (shudder). However, I think this is a really good place to start off with unit testing in Angular. One, because your important business logic should really be happening in controllers anyhow; And two, because it really is one of the easiest things to test in Angular.
Going forward, I will definitely try to add entries on unit testing things like services and directives.
