Over the last few years I’ve written several apps using Angular, but only after a project for work have I had to worry about unit testing them. Much like learning the framework itself, there are a number of techniques you need to pick up to be able to properly unit test an Angular app. Here are some common ones for testing controllers, services, and directives.
This post assumes the following:
- You’re running AngularJS 1.3 or greater
- You’re using Jasmine as your test framework
- You’re using Karma as your test runner
- You’re familiar with the above three things
In order to write about testing, I need something to test! So I whipped up a to-do list app called “To-Don’t” that uses some common Angular features, including:
- A controller
ListControllerwith the main application logic
- A service
TodoServicefor making HTTP calls to the backend
- A custom
ngEnterdirective to enhance DOM capabilities
The secret sauce to testing Angular apps is
angular-mocks, a separate module not included in the main
angular.js file. This is loaded in for unit tests only, as specified in the
files section of
angular-mocks will expose a number of objects and methods required for writing unit tests.
ToDont module, representing the To-Don’t Angular app, is initialized in
app.js like this:
In the unit tests, the
ToDont module is loaded in a Jasmine
beforeEach block using the
Controllers are where most of the application logic lives. To-Don’t only has one controller,
ListController, but your app might have one controller per route or page in your application.
Initialization and Dependency Injection
Angular uses dependency injection to provide dependencies to a component. For example,
ListController is initialized with its dependencies using the
In the tests, the controller’s dependencies must be injected manually. This is done with
inject() function. Below
ListController’s dependencies are manually injected into the test and a test controller is created, complete with a test
Behavior is added to a controller by attaching properties and methods to the
$scope object. When the app is running, actions such as method calls and changes to properties are handled by Angular’s own event processing capabilities. In the unit tests, however, Angular must be explicitly told that these actions are occurring. This is done with the
$scope.$apply() can also be called with a function as a parameter. Code in the body of the function will be updated as if it were running in an Angular app.
Mocking Out Services
Because any services will be tested separately, they can be replaced in the controller unit tests with mocks.
ListController utilizes the
TodoService to interact with the backend API, so that will be mocked out. Methods of
TodoService can be spied on individually and given mock behavior specific to each test.
Services are used to organize code into common functionality. In To-Don’t, the
TodoService contains all the logic for updating the to-do list and is consumed by
ListController. In this case,
TodoService is also responsible for making requests to the backend API using the built-in
Initializing a service is similar to creating a controller, using the either the
factory method (there’s a difference) and injecting dependencies.
In the tests the service instance is created using
inject, similar to the controller tests.
TodoService instance is now ready to be tested.
Mocking HTTP Requests
In the controller unit tests the focus was the controller logic, so the
TodoService calls were mocked out. In the service unit tests the focus is the
TodoService logic, so the HTTP calls it makes will need to be mocked out; the backend itself doesn’t need to be tested (at least not here).
angular-mocks provides a mock implementation of
$httpBackend, acting like a Jasmine spy for HTTP requests. In the tests,
$httpBackend is told to expect certain requests and respond to them as appropriate. Below is an example of testing a successful GET request.
In between tests, make sure to clear
$httpBackend to catch any unexpected results:
Directives are Angular’s way of teaching the DOM new tricks. To-Don’t expands on the DOM’s native capabilities with an
ngEnter directive that runs a callback function when the enter key is pressed within an input field. The directive is created using the
directive function and returns a link function.
Once a directive is defined, it is included in the HTML for the application:
This binds the
addItem() method of
ListController as the callback when the enter key is pressed on the input. All the magic of linking and binding the callback is handled by Angular.
Creating the Test Fixture
The directive can be tested by creating a fixture to interact with. The HTML will need to be compiled and registered with Angular manually using the
element() method and
compiled element can now be tested by simulating user interaction. The test below simulates the user pressing the enter key.
There’s much more to unit testing Angular apps than this post covers. Here are some materials for further reading:
- Angular Developer Guide: Unit Testing
- An Introduction To Unit Testing In AngularJS Applications
- Unit Testing in AngularJS: Services, Controllers & Providers
- Unit Testing Services in AngularJS for Fun and for Profit
- Ben Lesh: Angular JS - Unit Testing - Services
Subscribe via RSS