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