From 7f1e2e48467f80cc083d24b44f088620e4e7bcb6 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 6 Jun 2011 08:50:35 -0700 Subject: new batch of docs --- docs/content/guide/dev_guide.unit-testing.ngdoc | 323 ++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/content/guide/dev_guide.unit-testing.ngdoc (limited to 'docs/content/guide/dev_guide.unit-testing.ngdoc') diff --git a/docs/content/guide/dev_guide.unit-testing.ngdoc b/docs/content/guide/dev_guide.unit-testing.ngdoc new file mode 100644 index 00000000..d26a904e --- /dev/null +++ b/docs/content/guide/dev_guide.unit-testing.ngdoc @@ -0,0 +1,323 @@ +@workInProgress +@ngdoc overview +@name Developer Guide: Unit Testing +@description + + +JavaScript is a dynamically typed language which comes with great power of expression, but it also +come 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 do it. +# It is all about NOT mixing concerns +Unit testing as the name implies is about testing individual units of code. Unit tests try to +answer the question: Did I think about the logic correctly. Does the sort function order the list +in the right order. In order to answer such question it is very important that we can isolate it. +That is because when we are testing the sort function we don't want to be forced into crating +related pieces such as the DOM elements, or making any XHR calls in getting the data to sort. While +this may seem obvious it usually is very difficult to be able to call an individual function on a +typical project. The reason is that the developers often time mix concerns, and they end up with a +piece of code which does everything. It reads the data from XHR, it sorts it 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 crated abstraction 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 XHR, or create the right kind of DOM, or assert that your function has mutated the +DOM in the right way. 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 angular is not magic, which means if +you don't follow these, you may very well end up with an untestable application. + + +## Dependency Inject +There are several ways in which you can get a hold of a dependency: +1. You could create it using the `new` operator. +2. You could look for it in a well know place, also known as global singleton. +3. You could ask a registry (also known as service registry) for it. (But how do you get a hold of +the registry? Must likely by looking it up in a well know place. See #2) +4. You could expect that the it be handed to you. + + +Out of the list above only the last of is testable. Lets look at why: + + +### Using the `new` operator + + +While there is nothing wrong with the `new` operator fundamentally the issue is that calling a new +on a constructor permanently binds the call site to the type. For example lets say that we are +trying to instantiate an `XHR` so that we can get some data from the server. + + +
+function MyClass(){
+  this.doWork = function(){
+    var xhr = new XHR();
+    xhr.open(method, url, true);
+    xhr.onreadystatechange = function(){...}
+    xhr.send();
+  }
+}
+
+ + +The issue becomes, that in tests, we would very much like to instantiate a `MockXHR` which would +allow us to return fake data and simulate network failures. By calling `new XHR()` we are +permanently bound to the actual one, and there is no good way to replace it. Yes there is monkey +patching, that is a bad idea for many reasons, which is outside the scope of this document. + + +The class above is hard to test since we have to resort to monkey patching: +
+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 look for the service in a well known location. + + +
+function MyClass(){
+  this.doWork = function(){
+    global.xhr({
+      method:'...',
+      url:'...',
+      complete:function(response){ ... }
+    })
+  }
+}
+
+ + +While no new instance of dependency is being created, it is fundamentally the same as `new`, in +that there is no good way to intercept the call to `global.xhr` for testing purposes, other then +through monkey patching. The basic issue for testing is that global variable needs to be mutated in +order to replace it with call to a mock method. For further explanation why this is bad see: {@link +http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/ Brittle Global +State & Singletons} + + +The class above is hard to test since we have to change global state: +
+var oldXHR = glabal.xhr;
+glabal.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 as that this can be solved by having a registry for all of the services, and then +having the tests replace the services as needed. + + +
+function MyClass() {
+  var serviceRegistry = ????;
+  this.doWork = function(){
+    var xhr = serviceRegistry.get('xhr');
+    xhr({
+      method:'...',
+      url:'...',
+      complete:function(response){ ... }
+    })
+}
+
+ + +However, where dose the serviceRegistry come from? if it is: +* `new`-ed up, the the test has no chance to reset the services for testing +* global look-up, then the service returned is global as well (but resetting is easier, since +there is only one global variable to be reset). + + +The class above is hard to test since we have to change global state: +
+var oldServiceLocator = glabal.serviceLocator;
+glabal.serviceLocator.set('xhr', function mockXHR(){});
+var myClass = new MyClass();
+myClass.doWork();
+// assert that mockXHR got called with the right arguments
+glabal.serviceLocator = oldServiceLocator; // if you forget this bad things will happen
+
+ + + + +### Passing in Dependencies: +Lastly the dependency can be passed in. + + +
+function MyClass(xhr) {
+  this.doWork = function(){
+    xhr({
+      method:'...',
+      url:'...',
+      complete:function(response){ ... }
+    })
+}
+
+ + +This is the proferred way since the code makes no assumptions as to where the `xhr` comes from, +rather that who-ever crated the class was responsible for passing it in. Since the creator of the +class should be different code the the user of the class, it separates the responsibility of +creation from the logic, and that is what dependency-injection is in a nutshell. + + +The class above is very testable, since in the test we can write: +
+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 dev_guide.di dependency-injection} built in which makes the right thin +the easy thing 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, which is what we would like to test. If the logic +for your application is mixed in with DOM manipulation, it will be hard to test as in the example +below: + + +
+function PasswordController(){
+  // 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 testability, since it requires your test to have the right kind +of DOM present when the code executes. The test would look like this: + + +
+var input = $('');
+var span = $('');
+$('body').html('
') + .find('div') + .append(input) + .append(span); +var pc = new PasswordController(); +input.val('abc'); +pc.grade(); +expect(span.text()).toEqual('weak'); +$('body').html(''); +
+ + +In angular the controllers are strictly separated from the DOM manipulation logic which results in +a much easier testability story as can be seen in this example: + + +
+function PasswordCntrl(){
+  this.password = '';
+  this.grade = function(){
+    var size = this.password.length;
+    if (size > 8) {
+      this.strength = 'strong';
+    } else if (size > 3) {
+      this.strength = 'medium';
+    } else {
+      this.strength = 'weak';
+    }
+  };
+}
+
+ + +and the tests is straight forward + + +
+var pc = new PasswordController();
+pc.password('abc');
+pc.grade();
+expect(span.strength).toEqual('weak');
+
+ + +Notice that the test is not only much shorter but it is easier to follow what is going on. We say +that such a test tells a story, rather then asserting random bits which don't seem to be related. + + + + +## Filters +{@link api/angular.filter Filters} are functions which transform the data into user readable +format. They are important because they remove the formatting responsibility from the application +logic, further simplifying the application logic. + + +
+angular.filter('length', function(text){
+  return (''+(text||'')).length;
+});
+
+
+var length = angular.filter('length');
+expect(length(null)).toEqual(0);
+expect(length('abc')).toEqual(3);
+
+ + +## Directives +Directives in angular are responsible for updating the DOM when the state of the model changes. + + + + +## Mocks +oue +## Global State Isolation +oue +# Preferred way of Testing +uo +## JavaScriptTestDriver +ou +## Jasmine +ou +## Sample project +uoe -- cgit v1.2.3