JavaScript is a dynamic type language, which gives her strong performance ability, but also makes the compiler almost impossible to provide any help to developers. For this reason, we feel that writing any javascript code must have a powerful and complete set of tests. angular has many features that make it easier for us to test our applications. We should have no excuse not to write tests (this is it...).
1. It is all about NOT mixing concerns (it is all about avoiding the code relationship becoming complicated...)
Unit testing, as the name, is about testing a single "unit". Unit testing strives to answer these questions: Am I already correct in my logic considerations? Are the results obtained by the sorting method correct? To answer these questions, it is particularly important to separate them. This is because when we are testing the sorting method, we don't want to care about other related fragments, such as DOM elements or initiate XHR requests to get data, etc. Obviously, it is usually difficult to call a function separately in a typical project. The reason for this problem is that developers often make relationships complicated and end up making a snippet look like it can do everything. It gets the data through XHR, sorts the data, and then manipulates the DOM. Together with angular we can write better code more easily, so angular provides us with dependency injection of XHR (which we can simulate) and angular also creates abstractions that allow us to sort the model without having to manipulate the DOM. So, at the end, we can simply write a sorting method, and then create a data set through the test case for the sorting method to be used when testing, and then determine whether the result model meets expectations. The test does not require waiting for XHR to create the corresponding DOM and determine whether the function operates the DOM correctly. The core idea of angular includes the testability of code, but also requires us to do the right thing. angular is committed to simplifying the way to do the right thing, but angular is not magic, which means that we may end up with an untestable application if we don't follow the following points.
1. Dependency Inject
There are many ways to get the dependency resources: 1) we can use the new operator; 2) we use a well-known method called "global singleton"; 3) we can request it from the registry service (but how do we get a registry? You can check the following chapters); 4) we can expect it to be passed.
Of the methods listed above, only the last one is testable, let's see why:
1) Using the new operator
There are basically no errors when using the new operator, but the problem is that calling the constructor through new will permanently bind the caller to the type. For example, we try to instantiate an XHR object so that we can get some data from the server.
function MyClass() { this.doWork = function() { var xhr = new XRH(); xhr.open(method,url,true); xhr.onreadystatechange = function() {…}; xhr.send(); }}The problem is that when testing, we usually need to instantiate a virtual XHR that can return test data or network errors. By calling new XHR(), we permanently bind the real XHR and there is no good way to replace it. Of course, there is a bad remedy and there are many reasons to prove that it is a bad idea:
var oldXHR = XHR;XHR = new MockXHR() {};myClass.doWork();//Judge whether MockXHR calls XHR = oldXHR through normal parameters;//If you forget this step, it is easy to cause sad things.2) Global look-up
Another way to solve the problem is to obtain the dependency resources in a well-known place.
function MyClass() { this.doWork = function() { global.xhr({…}); };}Without creating an instance of a new dependent object, the problem is basically the same as new, and there is no good way to intercept global.xhr calls when testing. The most basic problem with testing is that global variables need to be changed to call virtual methods and are modified. To learn more about its disadvantages, visit here: http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/
The above code is difficult to test, so we must modify global state:
var oldXHR = global.xhr;global.xhr = function mockXHR(){…};var myClass = new MyClass();//Judge whether MockXHR calls global.xhr = oldXHR through normal parameters;//If you forget this step, it is easy to cause sad things.3) Service Registry
Having a registry with all services seems to solve the problem, and then replace the required service in the test code.
function MyClass() { var serviceRegistry = ???; this.doWork = function() { var xhr = serviceRegistry.get("xhr"); … };}But where does the serviceRegistry come from? If it is: * new-ed up, 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 text behind here is the same as garbled... I didn't understand)
According to this method, modify the above Class to the following method:
var oldServiceLocator = global.serviceLocator;global.serviceLocator.set('xhr', function mockXHR() {});var myClass = new MyClass();myClass.doWork();//Judge whether MockXHR calls global.serviceLocator = oldServiceLocator; //If you forget this step, it is easy to cause sad things.4) Passing in Dependencies
Finally, dependency resources can be passed in.
function MyClass(xhr) { this.doWork = function() { xhr({…}); };}This is the preferred method, because the code does not need to pay attention to where xhr comes from, nor does it care who created the xhr passed in. Therefore, the creator of the class can be encoded separately from the class consumer, which separates the responsibility of creation from the logic, which is an overview of dependency injection.
This class is easy to test, and we can write it like this in the test:
function xhrMock(args) {…}var myClass = new MyClass(xhrMock);myClass.doWrok();// Make some judgments... Through this test code, we can realize that no global variables are corrupted.Dependency-injection (//www.VeVB.COM/article/91775.htm) included with angular, code written in this way makes it easier to write test code. If we want to write code that is highly testable, we'd better use it.
2. Controllers
Logic makes every application unique, and that's what we want to test. If our logic is mixed with DOM operations, this will be as difficult to test as the following example:
function PasswordController() { // Get a reference to the DOM object 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 above code will encounter problems when testing because it requires us to have the correct DOM when executing the test. The test code will be as follows:
var input = $('<input type="text"/>');var span = $('<span>');$('body').html('<div>').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 controller strictly separates the DOM operation logic, which will greatly reduce the difficulty of writing test cases. Take a look at the following example:
function PasswordCntrl($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'; } };}The test code is straightforward:
var pc = new PasswordController($scope);pc.password('abc');pc.grade();expect($scope.strength).toEqual('weak');It is worth noting that the test code is not only more discontinuous, but is easier to track. We have always said that test cases are telling stories, not judging other irrelevant things.
3. Filters
filter(http://docs.angularjs.org/api/ng.$filter) is used to convert data into a user-friendly format. They are important because they separate the responsibility of converting formats from application logic, further simplifying application logic.
myModule.filter('length', function() { return function(text){ return (''+(text||'')).length; }});var length = $filter('length');expect(length(null)).toEqual(0);expect(length('abc')).toEqual(3);4. Directives
5. Mocks
6. Global State Isolation
7. Preferred way of Testing
8. JavascriptTestDriver
9. Jasmine
10. Sample project
We will continue to update related articles in the future. Thank you for your support for this site!