diff options
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. + | 
