Unit Testing
Aaron N. Smith, @aaron_n_smith
What is unit testing?
Yes really, let's cover this real quick.
Unit Testing Explained
(as quickly as possible)
- Tests of individual "units" of code
- Validate that various inputs produce expected outcomes
- Proves that a function, module, etc. works as expected
- Helps document valid conditions and assumptions
- Unit tests are necessary but not sufficient
- Unit tests prevent introduction of bugs
Unit Testing's Value
Those who think unit testing is a waste of time
probably haven't worked on a big project/team.
Upfront cost to prevent headaches down the road.
Why Angular?
"designed from the ground up to be testable"
Decent App Architecture
Good separation of concerns
Easy to break into discrete modules and units
Dependency Injection!!
DI ➡ mock implementations ➡ isolation
Dependency Injection != Service Locator Pattern
Karma
- Fantastic test runner built by the Angular team
- Test framework agnostic
- Supports Jasmine, Mocha, QUnit
- Run in one or many browsers (or headless)
ngMock Module
Built-in utilities to make unit testing easier and faster
Problem
Asynchronous code is hard to test
In other words: callback hell
Asynchronous code is hard to test
In other words: callback hell
Solution
ngMock allows core Angular services to
be controlled in a synchronous fashion
ngMock allows core Angular services to
be controlled in a synchronous fashion
Seed Projects
ngBoilerplate gets you started with...
- Clean, scalable project organization
- No need to write scripts or configuration to build app or execute tests
- Just define your dependencies (you're using bower, right?) and you're off and running
- Give it a try
Unit testing is valuable
AngularJS is testable
So let's dive in!
Step 1: Choose Test Framework
Jasmine, Mocha, QUnit ➡ All Supported
Jasmine is Karma's default
and conveniently
it's the only one I've worked with
it's the only one I've worked with
So let's go with Jasmine...
Jasmine in 30 Seconds
- Suites ➡ describe()
- Specs ➡ it()
- Setup/Teardown ➡ beforeEach(), afterEach()
- Matchers ➡ expect(a).toBeEqual(b)
-
Spies ➡ expect(fn).toHaveBeenCalled()
Example: Simplest Controller Ever
angular.module('MyModule').controller('MyCtrl', function($scope) {
$scope.value = 0;
$scope.maxValue = 2;
$scope.incrementValue = function() {
if ($scope.value < $scope.maxValue) {
$scope.value++;
} else {
$scope.value = 0;
}
};
});
Example: Corresponding Unit Test
describe('MyCtrl', function() {
var scope, controller;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
controller = $controller('MyCtrl', {$scope: scope});
}));
it('has correct initial value', function() {
expect(scope.value).toEqual(0); expect(scope.maxValue).toEqual(2);
});
it('increments correctly', function() {
scope.incrementValue(); expect(scope.value).toEqual(1);
scope.incrementValue(); expect(scope.value).toEqual(2);
scope.incrementValue(); expect(scope.value).toEqual(0);
});
});
Let's make it dynamic!
Example: Controller
angular.module('MyModule').controller('MyCtrl', function($scope, $http) {
$scope.incrementValue = function() {
$scope.value = $scope.value < $scope.maxValue ? $scope.value + 1 : 0;
};
$http.get('api/incrementor/config').success(function(data) {
$scope.value = data.initialValue;
$scope.maxValue = data.maxValue;
});
});
Testing $http Calls
ngMock provides $httpBackend service to help
Usage
Inject $httpBackend in place of $http
Inject $httpBackend in place of $http
Outcome
No XHRs in your unit tests ➡ faster + isolated ➡ good
No XHRs in your unit tests ➡ faster + isolated ➡ good
$httpBackend Key Components
// Verify that a request is made and handled (properly)
expect().respond(...);
// Verify that a response is handled (properly) if called
when(...).respond(...);
// Force synchronous execution of pending request(s)
flush();
Other $httpBackend Gems
// Make sure all expect() calls were made
afterEach($httpBackend.verifyNoOutstandingExpectation);
// Make sure there are no calls waiting to be flushed
afterEach($httpBackend.verifyNoOutstandingRequest);
// Use in complex, multi-step specs
$httpBackend.resetExpectations();
Example: Corresponding Test
describe('MyCtrl', function() {
var scope, createController, httpBackend;
beforeEach(inject(function($controller, $rootScope, $httpBackend) {
scope = $rootScope.$new();
httpBackend = $httpBackend;
createController = function() {
return $controller('MyCtrl', {$scope: scope, $http: $httpBackend});
};
}));
it('sets correct initial values', function() {
// Continued on next slide
});
});
Example: Test (Continued)
it('sets correct initial values', function() {
httpBackend.expectGET('api/incrementor/config').respond(200, {
initialValue: 0,
maxValue: 2
});
createController();
httpBackend.flush();
expect(scope.value).toEqual(0);
expect(scope.maxValue).toEqual(2);
});
Other bits of ngMock
$timeout and $interval are decorated with flush() methods
Testing Other Components
Services ➡ Easy, Make sure to clean up singletons!
Filters ➡ Easy, Tailor made for unit testing
Directives ➡ More difficult
Testing Filters
describe('Capitalize Filter', function() {
var filter;
beforeEach(inject(function($filter) {
filter = $filter('capitalize');
}));
it('capitalizes various strings', function() {
var result;
result = filter('unit tests');
expect(result).toEqual('Unit Tests');
result = filter('UNIT TESTS');
expect(result).toEqual('Unit Tests');
});
});
Tips for Directives
- Use templateUrl for all non-trivial templates
- Use html2js preprocessor and $templateCache
- Consider separating logic from DOM manipulation
using controller property - Wrap transclude directives in a div in your tests
it('tests myDirective', function() {
var element = $compile('<my-directive></my-directive>')($rootScope);
// Fire watches so template expressions are executed
$rootScope.$digest();
// Test element to verify DOM structure/contents
// Test functions and values on $rootScope like with a controller
});
Making your code testable
(Angular is NOT idiot proof)
- Do not manipulate DOM in controllers
-
Do not hide functions that need to be tested
- Keep functions small and purposeful
- Avoid monolithic directives
- Write your tests before you get too far along
Coverage Goals
Lots of variables.
Decide for yourself.
Where do e2e tests fit in?
End-to-end tests cover user stories
Unit tests cover them at a different level of granularity
Neither is likely to be sufficient on its own
Thank You!
Questions?
Aaron N. Smith, @aaron_n_smith
Unit Testing with AngularJS
By Aaron N. Smith
Unit Testing with AngularJS
Meetup presentation on unit testing AngularJS apps.
- 1,348