Unit Testing AngularJS Applications
by Ben Centra
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
Demo App
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
ListController
with the main application logic - A service
TodoService
for making HTTP calls to the backend - A custom
ngEnter
directive to enhance DOM capabilities
You can view the code on GitHub or try out the live demo.
Angular Mocks
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 karma.conf.js
:
angular-mocks
will expose a number of objects and methods required for writing unit tests.
Module Creation
The 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 module
function:
Controllers
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 controller
method.
In the tests, the controller’s dependencies must be injected manually. This is done with angular-mock
’s inject()
function. Below ListController
’s dependencies are manually injected into the test and a test controller is created, complete with a test $scope
object.
Dealing with $scope
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()
method.
$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
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 $http
service.
Initialization
Initializing a service is similar to creating a controller, using the either the service
or factory
method (there’s a difference) and injecting dependencies.
In TodoService.js:
In the tests the service instance is created using inject
, similar to the controller tests.
The 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
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.
In ngEnter.js:
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 $compile
service.
In ngEnter-spec.js:
The final compiled
element can now be tested by simulating user interaction. The test below simulates the user pressing the enter key.
In ngEnter-spec.js:
Resources
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
Don’t forget to check out the source for To-Don’t on GitHub and try out the live demo!
Subscribe via RSS