aboutsummaryrefslogtreecommitdiffstats
path: root/docs/content/guide/unit-testing.ngdoc
diff options
context:
space:
mode:
Diffstat (limited to 'docs/content/guide/unit-testing.ngdoc')
-rw-r--r--docs/content/guide/unit-testing.ngdoc343
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.
+