diff options
| author | Igor Minar | 2011-06-06 08:50:35 -0700 | 
|---|---|---|
| committer | Igor Minar | 2011-06-06 22:52:02 -0700 | 
| commit | 7f1e2e48467f80cc083d24b44f088620e4e7bcb6 (patch) | |
| tree | 731a91366c5780985be6d4c5ddbe34e307d5cb70 /docs/content/guide/dev_guide.unit-testing.ngdoc | |
| parent | 5533e48dead5cff3107e72ee80bf0f19df77c1e9 (diff) | |
| download | angular.js-7f1e2e48467f80cc083d24b44f088620e4e7bcb6.tar.bz2 | |
new batch of docs
Diffstat (limited to 'docs/content/guide/dev_guide.unit-testing.ngdoc')
| -rw-r--r-- | docs/content/guide/dev_guide.unit-testing.ngdoc | 323 | 
1 files changed, 323 insertions, 0 deletions
| 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. + + +<pre> +function MyClass(){ +  this.doWork = function(){ +    var xhr = new XHR(); +    xhr.open(method, url, true); +    xhr.onreadystatechange = function(){...} +    xhr.send(); +  } +} +</pre> + + +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: +<pre> +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 +</pre> + + + + +### Global look-up: +Another way to approach the problem is look for the service in a well known location. + + +<pre> +function MyClass(){ +  this.doWork = function(){ +    global.xhr({ +      method:'...', +      url:'...', +      complete:function(response){ ... } +    }) +  } +} +</pre> + + +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: +<pre> +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 +</pre> + + + + +### 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. + + +<pre> +function MyClass() { +  var serviceRegistry = ????; +  this.doWork = function(){ +    var xhr = serviceRegistry.get('xhr'); +    xhr({ +      method:'...', +      url:'...', +      complete:function(response){ ... } +    }) +} +</pre> + + +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: +<pre> +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 +</pre> + + + + +### Passing in Dependencies: +Lastly the dependency can be passed in. + + +<pre> +function MyClass(xhr) { +  this.doWork = function(){ +    xhr({ +      method:'...', +      url:'...', +      complete:function(response){ ... } +    }) +} +</pre> + + +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: +<pre> +function xhrMock(args) {...} +var myClass = new MyClass(xhrMock); +myClass.doWork(); +// assert that xhrMock got called with the right arguments +</pre> + + +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: + + +<pre> +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); +  } +} +</pre> + + +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: + + +<pre> +var input = $('<input type="text"/>'); +var span = $('<span>'); +$('body').html('<div class="ex1">') +  .find('div') +    .append(input) +    .append(span); +var pc = new PasswordController(); +input.val('abc'); +pc.grade(); +expect(span.text()).toEqual('weak'); +$('body').html(''); +</pre> + + +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: + + +<pre> +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'; +    } +  }; +} +</pre> + + +and the tests is straight forward + + +<pre> +var pc = new PasswordController(); +pc.password('abc'); +pc.grade(); +expect(span.strength).toEqual('weak'); +</pre> + + +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. + + +<pre> +angular.filter('length', function(text){ +  return (''+(text||'')).length; +}); + + +var length = angular.filter('length'); +expect(length(null)).toEqual(0); +expect(length('abc')).toEqual(3); +</pre> + + +## 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 | 
