diff options
| author | Brian Ford | 2014-03-03 12:35:00 -0800 |
|---|---|---|
| committer | Brian Ford | 2014-03-03 12:35:00 -0800 |
| commit | fde61423cf13cb57be02a6f79581cb96c34335f9 (patch) | |
| tree | 7ac98e14d7ae67f2b69f76a2690d1e00459ca8c8 /docs/content/guide/unit-testing.ngdoc | |
| parent | c29d21f4cdc969991771d1e75c3a799eca7bf41a (diff) | |
| download | angular.js-fde61423cf13cb57be02a6f79581cb96c34335f9.tar.bz2 | |
docs(guide/unit-testing): rename and fix formatting
Diffstat (limited to 'docs/content/guide/unit-testing.ngdoc')
| -rw-r--r-- | docs/content/guide/unit-testing.ngdoc | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/docs/content/guide/unit-testing.ngdoc b/docs/content/guide/unit-testing.ngdoc new file mode 100644 index 00000000..8d41bedf --- /dev/null +++ b/docs/content/guide/unit-testing.ngdoc @@ -0,0 +1,343 @@ +@ngdoc overview +@name Unit Testing +@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 in getting 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 (which you can mock out) and we created abstractions which +allow you to sort your model without having to resort to manipulating the DOM. So that in the end, +it is easy to write a sort function which sorts some data, so that your test can create a data set, +apply the function, and assert that the resulting model is in the correct order. The test does not +have to wait for the XHR response to arrive, create the right kind of test DOM, nor assert that your +function has mutated the DOM in the right way. + +## 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 +There are several ways in which you can get a hold of a dependency. You can: +1. Create it using the `new` operator. +2. Look for it in a well-known place, also known as a global singleton. +3. Ask a registry (also known as service registry) for it. (But how do you get a hold of +the registry? Most likely by looking it up in a well known place. See #2.) +4. Expect it to be handed to you. + +Out of the four options in the list above, only the last one is testable. Let's look at why: + +### Using the `new` operator + +While there is nothing wrong with the `new` operator fundamentally, a problem arises when calling `new` +on a constructor. This permanently binds the call site to the type. For example, lets say that we try to +instantiate an `XHR` that will retrieve data from the server. + +```js +function MyClass() { + this.doWork = function() { + var xhr = new XHR(); + xhr.open(method, url, true); + xhr.onreadystatechange = function() {...} + xhr.send(); + } +} +``` + +A problem surfaces in tests when we would like to instantiate a `MockXHR` that would +allow us to return fake data and simulate network failures. By calling `new XHR()` we are +permanently bound to the actual XHR and there is no way to replace it. Yes, we could monkey +patch, but that is a bad idea for many reasons which are outside the scope of this document. + +Here's an example of how the class above becomes hard to test when resorting to monkey patching: + +```js +var oldXHR = XHR; +XHR = function MockXHR() {}; +var myClass = new MyClass(); +myClass.doWork(); +// assert that MockXHR got called with the right arguments +XHR = oldXHR; // if you forget this bad things will happen +``` + + +### Global look-up: +Another way to approach the problem is to look for the service in a well-known location. + +```js +function MyClass() { + this.doWork = function() { + global.xhr({ + method:'...', + url:'...', + complete:function(response){ ... } + }) + } +} +``` + +While no new dependency instance is created, it is fundamentally the same as `new` in +that no way exists to intercept the call to `global.xhr` for testing purposes, other then +through monkey patching. The basic issue for testing is that a global variable needs to be mutated in +order to replace it with call to a mock method. For further explanation of why this is bad see: [Brittle Global +State & Singletons](http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/) + +The class above is hard to test since we have to change the global state: + +```js +var oldXHR = global.xhr; +global.xhr = function mockXHR() {}; +var myClass = new MyClass(); +myClass.doWork(); +// assert that mockXHR got called with the right arguments +global.xhr = oldXHR; // if you forget this bad things will happen +``` + + +### Service Registry: + +It may seem that this can be solved by having a registry of all the services and then +having the tests replace the services as needed. + +```js +function MyClass() { + var serviceRegistry = ????; + this.doWork = function() { + var xhr = serviceRegistry.get('xhr'); + xhr({ + method:'...', + url:'...', + complete:function(response){ ... } + }) +} +``` + +However, where does the serviceRegistry come from? If it is: +* `new`-ed up, the test has no chance to reset the services for testing. +* a global look-up then the service returned is global as well (but resetting is easier, since +only one global variable exists to be reset). + +The class above is hard to test since we have to change the global state: + +```js +var oldServiceLocator = global.serviceLocator; +global.serviceLocator.set('xhr', function mockXHR() {}); +var myClass = new MyClass(); +myClass.doWork(); +// assert that mockXHR got called with the right arguments +global.serviceLocator = oldServiceLocator; // if you forget this bad things will happen +``` + + +### Passing in Dependencies: +Last, the dependency can be passed in. + +```js +function MyClass(xhr) { + this.doWork = function() { + xhr({ + method:'...', + url:'...', + complete:function(response){ ... } + }) +} +``` + +This is the preferred method since the code makes no assumptions about the origin of `xhr` and cares +instead about whoever created the class responsible for passing it in. Since the creator of the +class should be different code than the user of the class, it separates the responsibility of +creation from the logic. This is dependency-injection in a nutshell. + +The class above is testable, since in the test we can write: + +```js +function xhrMock(args) {...} +var myClass = new MyClass(xhrMock); +myClass.doWork(); +// assert that xhrMock got called with the right arguments +``` + +Notice that no global variables were harmed in the writing of this test. + +Angular comes with {@link di dependency injection} built-in, making the right thing +easy to do, but you still need to do it if you wish to take advantage of the testability story. + +## Controllers +What makes each application unique is its logic, and the logic is what we would like to test. If the logic +for your application contains DOM manipulation, it will be hard to test. See the example +below: + +```js +function PasswordCtrl() { + // get references to DOM elements + var msg = $('.ex1 span'); + var input = $('.ex1 input'); + var strength; + + this.grade = function() { + msg.removeClass(strength); + var pwd = input.val(); + password.text(pwd); + if (pwd.length > 8) { + strength = 'strong'; + } else if (pwd.length > 3) { + strength = 'medium'; + } else { + strength = 'weak'; + } + msg + .addClass(strength) + .text(strength); + } +} +``` + +The code above is problematic from a testability point of view since it requires your test to have the right kind +of DOM present when the code executes. The test would look like this: + +```js +var input = $('<input type="text"/>'); +var span = $('<span>'); +$('body').html('<div class="ex1">') + .find('div') + .append(input) + .append(span); +var pc = new PasswordCtrl(); +input.val('abc'); +pc.grade(); +expect(span.text()).toEqual('weak'); +$('body').empty(); +``` + +In angular the controllers are strictly separated from the DOM manipulation logic and this results in +a much easier testability story as the following example shows: + +```js +function PasswordCtrl($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'; + } + }; +} +``` + +and the test is straight forward: + +```js +var $scope = {}; +var pc = $controller('PasswordCtrl', { $scope: $scope }); +$scope.password = 'abc'; +$scope.grade(); +expect($scope.strength).toEqual('weak'); +``` + +Notice that the test is not only much shorter, it is also easier to follow what is happening. We say +that such a test tells a story, rather then asserting random bits which don't seem to be related. + +## 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; + } +}); + +var length = $filter('length'); +expect(length(null)).toEqual(0); +expect(length('abc')).toEqual(3); +``` + +## 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; + var $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. + + +## Sample project +See the [angular-seed](https://github.com/angular/angular-seed) project for an example. + |
