mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-05 17:01:19 +08:00
This commit adds to the unit testing guide: - an explicit section on additional libraries: Karma, Jasmine and angular-mocks and link to the docs for those projects too. Explain the benefit and use case for each of these libaries - fully featured test examples and add more documentation around them, in particular the controller test - a clear separation between the section on principles of testing and the actual tutorial on writing a test Closes #8220
428 lines
16 KiB
Plaintext
428 lines
16 KiB
Plaintext
@ngdoc overview
|
|
@name Unit Testing
|
|
@sortOrder 410
|
|
@description
|
|
|
|
JavaScript is a dynamically typed language which comes with great power of expression, but it also
|
|
comes with almost no help from the compiler. For this reason we feel very strongly that any code
|
|
written in JavaScript needs to come with a strong set of tests. We have built many features into
|
|
Angular which makes testing your Angular applications easy. So there is no excuse for not testing.
|
|
|
|
## Separation of Concerns
|
|
|
|
Unit testing, as the name implies, is about testing individual units of code. Unit tests try to
|
|
answer questions such as "Did I think about the logic correctly?" or "Does the sort function order
|
|
the list in the right order?"
|
|
|
|
In order to answer such a question it is very important that we can isolate the unit of code under test.
|
|
That is because when we are testing the sort function we don't want to be forced into creating
|
|
related pieces such as the DOM elements, or making any XHR calls to fetch the data to sort.
|
|
|
|
While this may seem obvious it can be very difficult to call an individual function on a
|
|
typical project. The reason is that the developers often mix concerns resulting in a
|
|
piece of code which does everything. It makes an XHR request, it sorts the response data and then it
|
|
manipulates the DOM.
|
|
|
|
With Angular we try to make it easy for you to do the right thing, and so we
|
|
provide dependency injection for your XHR requests, which can be mocked, and we provide abstractions which
|
|
allow you to test your model without having to resort to manipulating the DOM. The test can then
|
|
assert that the data has been sorted without having to create or look at the state of the DOM or
|
|
wait for any XHR requests to return data. The individual sort function can be tested in isolation.
|
|
|
|
## With great power comes great responsibility
|
|
|
|
Angular is written with testability in mind, but it still requires that you do the right thing.
|
|
We tried to make the right thing easy, but if you ignore these guidelines you may end up with an
|
|
untestable application.
|
|
|
|
## Dependency Injection
|
|
|
|
Angular comes with {@link di dependency injection} built-in, which makes testing components much
|
|
easier, because you can pass in a component's dependencies and stub or mock them as you wish.
|
|
|
|
Components that have their dependencies injected allow them to be easily mocked on a test by
|
|
test basis, without having to mess with any global variables that could inadvertently affect
|
|
another test.
|
|
|
|
## Additional tools for testing Angular applications
|
|
|
|
For testing Angular applications there are certain tools that you should use that will make testing much
|
|
easier to set up and run.
|
|
|
|
### Karma
|
|
|
|
[Karma](http://karma-runner.github.io/) is a JavaScript command line tool that can be used to spawn
|
|
a web server which loads your application's source code and executes your tests. You can configure
|
|
Karma to run against a number of browsers, which is useful for being confident that your application
|
|
works on all browsers you need to support. Karma is executed on the command line and will display
|
|
the results of your tests on the command line once they have run in the browser.
|
|
|
|
Karma is a NodeJS application, and should be installed through npm. Full installation instructions
|
|
are available on [the Karma website](http://karma-runner.github.io/0.12/intro/installation.html).
|
|
|
|
### Jasmine
|
|
|
|
[Jasmine](http://jasmine.github.io/1.3/introduction.html) is a test driven development framework for
|
|
JavaScript that has become the most popular choice for testing Angular applications. Jasmine
|
|
provides functions to help with structuring your tests and also making assertions. As your tests
|
|
grow, keeping them well structured and documented is vital, and Jasmine helps achieve this.
|
|
|
|
In Jasmine we use the `describe` function to group our tests together:
|
|
|
|
```js
|
|
describe("sorting the list of users", function() {
|
|
// individual tests go here
|
|
});
|
|
```
|
|
|
|
And then each individual test is defined within a call to the `it` function:
|
|
|
|
```js
|
|
describe('sorting the list of users', function() {
|
|
it('sorts in descending order by default', function() {
|
|
// your test assertion goes here
|
|
});
|
|
});
|
|
```
|
|
|
|
Grouping related tests within `describe` blocks and describing each individual test within an
|
|
`it` call keeps your tests self documenting.
|
|
|
|
Finally, Jasmine provides matchers which let you make assertions:
|
|
|
|
```js
|
|
describe('sorting the list of users', function() {
|
|
it('sorts in descending order by default', function() {
|
|
var users = ['jack', 'igor', 'jeff'];
|
|
var sorted = sortUsers(users);
|
|
expect(sorted).toEqual(['jeff', 'jack', 'igor']);
|
|
});
|
|
});
|
|
```
|
|
|
|
Jasmine comes with a number of matchers that help you make a variety of assertions. You should [read
|
|
the Jasmine documentation](http://jasmine.github.io/1.3/introduction.html#section-Matchers) to see
|
|
what they are. To use Jasmine with Karma, we use the
|
|
[karma-jasmine](https://github.com/karma-runner/karma-jasmine) test runner.
|
|
|
|
### angular-mocks
|
|
|
|
Angular also provides the {@link ngMock} module, which provides mocking for your tests. This is used
|
|
to inject and mock Angular services within unit tests. In addition, it is able to extend other
|
|
modules so they are synchronous. Having tests synchronous keeps them much cleaner and easier to work
|
|
with. One of the most useful parts of ngMock is {@link ngMock.$httpBackend}, which lets us mock XHR
|
|
requests in tests, and return sample data instead.
|
|
|
|
## Testing a Controller
|
|
|
|
Because Angular separates logic from the view layer, it keeps controllers easy to test. Let's take a
|
|
look at how we might test the controller below, which provides `$scope.grade`, which sets a property
|
|
on the scope based on the length of the password.
|
|
|
|
```js
|
|
angular.module('app', [])
|
|
.controller('PasswordController', function PasswordController($scope) {
|
|
$scope.password = '';
|
|
$scope.grade = function() {
|
|
var size = $scope.password.length;
|
|
if (size > 8) {
|
|
$scope.strength = 'strong';
|
|
} else if (size > 3) {
|
|
$scope.strength = 'medium';
|
|
} else {
|
|
$scope.strength = 'weak';
|
|
}
|
|
};
|
|
});
|
|
```
|
|
|
|
Because controllers are not available on the global scope, we need to use {@link
|
|
angular.mock.inject} to inject our controller first. The first step is to use the `module` function,
|
|
which is provided by angular-mocks. This loads in the module it's given, so it is available in your
|
|
tests. We pass this into `beforeEach`, which is a function Jasmine provides that lets us run code
|
|
before each test. Then we can use `inject` to access `$controller`, the service that is responsible
|
|
for instantiating controllers.
|
|
|
|
```js
|
|
describe('PasswordController', function() {
|
|
beforeEach(module('app'));
|
|
|
|
var $controller;
|
|
|
|
beforeEach(inject(function(_$controller_){
|
|
// The injector unwraps the underscores (_) from around the parameter names when matching
|
|
$controller = _$controller_;
|
|
}));
|
|
|
|
describe('$scope.grade', function() {
|
|
it('sets the strength to "strong" if the password length is >8 chars', function() {
|
|
var $scope = {};
|
|
var controller = $controller('PasswordController', { $scope: $scope });
|
|
$scope.password = 'longerthaneightchars';
|
|
$scope.grade();
|
|
expect($scope.strength).toEqual('strong');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
Notice how by nesting the `describe` calls and being descriptive when calling them with strings, the
|
|
test is very clear. It documents exactly what it is testing, and at a glance you can quickly see
|
|
what is happening. Now let's add the test for when the password is less than three characters, which
|
|
should see `$scope.strength` set to "weak":
|
|
|
|
```js
|
|
describe('PasswordController', function() {
|
|
beforeEach(module('app'));
|
|
|
|
var $controller;
|
|
|
|
beforeEach(inject(function(_$controller_){
|
|
// The injector unwraps the underscores (_) from around the parameter names when matching
|
|
$controller = _$controller_;
|
|
}));
|
|
|
|
describe('$scope.grade', function() {
|
|
it('sets the strength to "strong" if the password length is >8 chars', function() {
|
|
var $scope = {};
|
|
var controller = $controller('PasswordController', { $scope: $scope });
|
|
$scope.password = 'longerthaneightchars';
|
|
$scope.grade();
|
|
expect($scope.strength).toEqual('strong');
|
|
});
|
|
|
|
it('sets the strength to "weak" if the password length <3 chars', function() {
|
|
var $scope = {};
|
|
var controller = $controller('PasswordController', { $scope: $scope });
|
|
$scope.password = 'a';
|
|
$scope.grade();
|
|
expect($scope.strength).toEqual('weak');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
Now we have two tests, but notice the duplication between the tests. Both have to
|
|
create the `$scope` variable and create the controller. As we add new tests, this duplication is
|
|
only going to get worse. Thankfully, Jasmine provides `beforeEach`, which lets us run a function
|
|
before each individual test. Let's see how that would tidy up our tests:
|
|
|
|
```js
|
|
describe('PasswordController', function() {
|
|
beforeEach(module('app'));
|
|
|
|
var $controller;
|
|
|
|
beforeEach(inject(function(_$controller_){
|
|
// The injector unwraps the underscores (_) from around the parameter names when matching
|
|
$controller = _$controller_;
|
|
}));
|
|
|
|
describe('$scope.grade', function() {
|
|
var $scope, controller;
|
|
|
|
beforeEach(function() {
|
|
$scope = {};
|
|
controller = $controller('PasswordController', { $scope: $scope });
|
|
});
|
|
|
|
it('sets the strength to "strong" if the password length is >8 chars', function() {
|
|
$scope.password = 'longerthaneightchars';
|
|
$scope.grade();
|
|
expect($scope.strength).toEqual('strong');
|
|
});
|
|
|
|
it('sets the strength to "weak" if the password length <3 chars', function() {
|
|
$scope.password = 'a';
|
|
$scope.grade();
|
|
expect($scope.strength).toEqual('weak');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
We've moved the duplication out and into the `beforeEach` block. Each individual test now
|
|
only contains the code specific to that test, and not code that is general across all tests. As you
|
|
expand your tests, keep an eye out for locations where you can use `beforeEach` to tidy up tests.
|
|
`beforeEach` isn't the only function of this sort that Jasmine provides, and the [documentation
|
|
lists the others](http://jasmine.github.io/1.3/introduction.html#section-Setup_and_Teardown).
|
|
|
|
## Testing Filters
|
|
{@link ng.$filterProvider Filters} are functions which transform the data into a user readable
|
|
format. They are important because they remove the formatting responsibility from the application
|
|
logic, further simplifying the application logic.
|
|
|
|
```js
|
|
myModule.filter('length', function() {
|
|
return function(text) {
|
|
return ('' + (text || '')).length;
|
|
}
|
|
});
|
|
|
|
describe('length filter', function() {
|
|
it('returns 0 when given null', function() {
|
|
var length = $filter('length');
|
|
expect(length(null)).toEqual(0);
|
|
});
|
|
|
|
it('returns the correct value when given a string of chars', function() {
|
|
var length = $filter('length');
|
|
expect(length('abc')).toEqual(3);
|
|
});
|
|
});
|
|
```
|
|
|
|
## Testing Directives
|
|
Directives in angular are responsible for encapsulating complex functionality within custom HTML tags,
|
|
attributes, classes or comments. Unit tests are very important for directives because the components
|
|
you create with directives may be used throughout your application and in many different contexts.
|
|
|
|
### Simple HTML Element Directive
|
|
|
|
Let's start with an angular app with no dependencies.
|
|
|
|
```js
|
|
var app = angular.module('myApp', []);
|
|
```
|
|
|
|
Now we can add a directive to our app.
|
|
|
|
```js
|
|
app.directive('aGreatEye', function () {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
template: '<h1>lidless, wreathed in flame, {{1 + 1}} times</h1>'
|
|
};
|
|
});
|
|
```
|
|
|
|
This directive is used as a tag `<a-great-eye></a-great-eye>`. It replaces the entire tag with the
|
|
template `<h1>lidless, wreathed in flame, {{1 + 1}} times</h1>`. Now we are going to write a jasmine unit test to
|
|
verify this functionality. Note that the expression `{{1 + 1}}` times will also be evaluated in the rendered content.
|
|
|
|
```js
|
|
describe('Unit testing great quotes', function() {
|
|
var $compile,
|
|
$rootScope;
|
|
|
|
// Load the myApp module, which contains the directive
|
|
beforeEach(module('myApp'));
|
|
|
|
// Store references to $rootScope and $compile
|
|
// so they are available to all tests in this describe block
|
|
beforeEach(inject(function(_$compile_, _$rootScope_){
|
|
// The injector unwraps the underscores (_) from around the parameter names when matching
|
|
$compile = _$compile_;
|
|
$rootScope = _$rootScope_;
|
|
}));
|
|
|
|
it('Replaces the element with the appropriate content', function() {
|
|
// Compile a piece of HTML containing the directive
|
|
var element = $compile("<a-great-eye></a-great-eye>")($rootScope);
|
|
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
|
|
$rootScope.$digest();
|
|
// Check that the compiled element contains the templated content
|
|
expect(element.html()).toContain("lidless, wreathed in flame, 2 times");
|
|
});
|
|
});
|
|
```
|
|
|
|
We inject the $compile service and $rootScope before each jasmine test. The $compile service is used
|
|
to render the aGreatEye directive. After rendering the directive we ensure that the directive has
|
|
replaced the content and "lidless, wreathed in flame, 2 times" is present.
|
|
|
|
<div class="alert alert-info">
|
|
**Underscore notation**:
|
|
|
|
The use of the underscore notation (e.g.: `_$rootScope_`) is a convention wide spread in AngularJS
|
|
community to keep the variable names clean in your tests. That's why the
|
|
{@link $injector} strips out the leading and the trailing underscores when
|
|
matching the parameters. The underscore rule applies ***only*** if the name starts **and** ends with
|
|
exactly one underscore, otherwise no replacing happens.
|
|
</div>
|
|
|
|
### Testing Transclusion Directives
|
|
|
|
Directives that use transclusion are treated specially by the compiler. Before their compile
|
|
function is called, the contents of the directive's element are removed from the element and
|
|
provided via a transclusion function. The directive's template is then appended to the directive's
|
|
element, to which it can then insert the transcluded content into its template.
|
|
|
|
|
|
Before compilation:
|
|
```html
|
|
<div translude-directive>
|
|
Some transcluded content
|
|
</div>
|
|
```
|
|
|
|
After transclusion extraction:
|
|
```html
|
|
<div transclude-directive></div>
|
|
```
|
|
|
|
After compilation:
|
|
```html
|
|
<div transclude-directive>
|
|
Some Template
|
|
<span ng-transclude>Some transcluded content</span>
|
|
</div>
|
|
```
|
|
|
|
If the directive is using 'element' transclusion, the compiler will actually remove the
|
|
directive's entire element from the DOM and replace it with a comment node. The compiler then
|
|
inserts the directive's template "after" this comment node, as a sibling.
|
|
|
|
Before compilation
|
|
```html
|
|
<div element-transclude>
|
|
Some Content
|
|
</div>
|
|
```
|
|
|
|
After transclusion extraction
|
|
```html
|
|
<!-- elementTransclude -->
|
|
```
|
|
|
|
After compilation:
|
|
```html
|
|
<!-- elementTransclude -->
|
|
<div element-transclude>
|
|
Some Template
|
|
<span ng-transclude>Some transcluded content</span>
|
|
</div>
|
|
```
|
|
|
|
It is important to be aware of this when writing tests for directives that use 'element'
|
|
transclusion. If you place the directive on the root element of the DOM fragment that you
|
|
pass to {@link $compile}, then the DOM node returned from the linking function will be the
|
|
comment node and you will lose the ability to access the template and transcluded content.
|
|
|
|
```javascript
|
|
var node = $compile('<div element-transclude></div>')($rootScope);
|
|
expect(node[0].nodeType).toEqual(node.COMMENT_NODE);
|
|
expect(node[1]).toBeUndefined();
|
|
```
|
|
|
|
To cope with this you simply ensure that your 'element' transclude directive is wrapped in an
|
|
element, such as a `<div>`.
|
|
|
|
```javascript
|
|
var node = $compile('<div><div element-transclude></div></div>')($rootScope);
|
|
var contents = node.contents();
|
|
expect(contents[0].nodeType).toEqual(node.COMMENT_NODE);
|
|
expect(contents[1].nodeType).toEqual(node.ELEMENT_NODE);
|
|
```
|
|
|
|
### Testing Directives With External Templates
|
|
|
|
If your directive uses `templateUrl`, consider using
|
|
[karma-ng-html2js-preprocessor](https://github.com/karma-runner/karma-ng-html2js-preprocessor)
|
|
to pre-compile HTML templates and thus avoid having to load them over HTTP during test execution.
|
|
Otherwise you may run into issues if the test directory hierarchy differs from the application's.
|
|
|
|
## Sample project
|
|
See the [angular-seed](https://github.com/angular/angular-seed) project for an example.
|