There are a few aspects of unit testing AngularJS controllers that have made me uncomfortable over time. In this post I’ll describe some of these issues and what I’ve been trying on my current project to put the code in an acceptable state, as well as some general tips I’ve found useful.
One approach to testing a controller with Jasmine is to use the module and inject helpers in a beforeEach block to create the dependencies for a controller. Most controllers will need multiple beforeEach blocks to setup the environment for different testing scenarios (there is the happy day scenario, the failing network scenario, the bad input scenario, etc). If you follow the pattern found in demo projects you’ll start to see too much code duplication in these setup blocks.
I’m comfortable with some amount of duplication inside of tests, as are others, however, the constant use of inject to bring in dependencies that 80% of the tests need becomes a noisy tax.
What I’ve been doing recently is using a single inject call per spec file in an opening beforeEach block. This block manually hoists all dependencies into the scope for other tests, and also runs $http backend verifications after each test, even if they aren’t needed in every test.
var $rootScope, $controller, $q, $httpBackend, appConfig, scope; beforeEach(inject(function (_$rootScope_, _$controller_, _$q_, _$httpBackend_, _appConfig_) { $q = _$q_; $rootScope = _$rootScope_; $controller = _$controller_; $httpBackend = _$httpBackend_; appConfig = _appConfig_; scope = $rootScope.$new(); })); afterEach(function () { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); });
Now each scenario and the tests inside have all the core objects they need to setup the proper environment for testing. There is even a fresh scope object waiting for every test, and the rest of the test code no longer needs inject.
describe("the reportListController", function () { beforeEach(function () { $httpBackend.when("GET", appConfig.reportUrl).respond([{}, {}, {}]); $controller("reportListController", { $scope: scope, }); $httpBackend.flush(); }); it("should retrieve the reports to list", function() { expect(scope.reports.length).toBe(3); }); });
I believe this approach has been beneficial. The tests are leaner, easier to read, and easier to maintain.
Notice the injected function in the opening beforeEach uses parameter names like _$rootScope_ and _$q_. The inject function knows how to strip the underscores to get to the real service names, and since the parameters use underscores the variables in the outer scope can use pretty names like $rootScope and $q.
Sometimes I’ve seen examples using $controller that pass every dependency in the second parameter.
$controller("reportListController", { $scope: scope, $http: $http, $q: $q });
Chances are the above code only really needs to pass $scope, because the injector will fill in the rest of the services as appropriate.
$controller("reportListController", { $scope: scope });
Again there is less test code and the code is easier to maintain. Controller dependencies can change, but the test code doesn’t. Building your own mocks library like angular-mocks for custom services also helps this scenario, too. If you don’t need to “own” the dependency in a test, don’t bother to setup and pass the dependency to $controller.
Perhaps controversial, but I’ve started to write tests that do not mock services. Instead, I test a controller and most of the services the controller requires as a single unit. First let me give some details on what I mean, and then explain why I think this works well.
Let’s assume we have a reportListController that uses $scope, as well as two custom services that themselves use $http behind the scenes to communicate with the web server. Instead of having long complicated scenario setups with mocks and stubs, I usually only focus on a mock HTTP backend and scope.
$httpBackend.when("GET", appConfig.reportUrl).respond(reports); $controller("reportListController", { $scope: scope }); $httpBackend.flush();
This is a scenario for deleting reports, and the tests are relatively simple.
it("should delete the report", function () { $httpBackend.when("DELETE", appConfig.reportUrl + "/1").respond(200); scope.delete(reports[0]); $httpBackend.flush(); expect(scope.reports.length).toBe(1); }); it("should show a message", function () { $httpBackend.when("DELETE", appConfig.reportUrl + "/1").respond(200); scope.delete(reports[0]); $httpBackend.flush(); expect($rootScope.alerts.length).toBe(1); });
These two tests are exercising a number of logical pieces in the application. It’s not just testing the controller but also the model and also how the model interacts with two different services and how those services interact and respond to HTTP traffic.
I’m sure a few people will think these tests are blasphemous and the model and the services should be tested in isolation. However, I believe it is this type of right versus wrong thinking centered around “best practices” that severely limit the acceptance of unit testing in more circles. After years of mock object frameworks in other languages I’ve learned to avoid mocks whenever possible. Mock objects and mock methods generally:
What I want to test in these scenarios is how the code inside the controller interacts with the services, because most of the logic inside is focused on orchestrating the underlying services to perform useful work. The code inside has to call service methods at the right time, and handle promises appropriately. I want to be able to change the implementation details without reworking the tests. These test work by providing an input (delete this report), and looking at the output (the number of reports), and only needs to provide some fake HTTP message processing to fill in the gaps.
If I had written two mock services for the controller and tested the services in isolation, I’d have more test code but less confidence that the system actually works.
Testing controllers that make service calls when instantiated can be a bit tricky, because everything has to be setup and in place before using the $controller service to instantiate the controller itself.
Using promise resolves in a route definition not only makes for an arguably better user experience, it also makes for easier controller testing because the controller is given everything it needs to get started. Both ui.router and ngRouter support resolves in a route definition, but since this post is already long in the tooth. We’ll look at using resolves in a future post.