javascript是一門動態類型語言,這給她帶來了很強的表現能力,但同時也使編譯器幾乎不能給開發人員提供任何協助。因為這個原因,我們感受到編寫任何javascript代碼都必須有一套強大完整的測試。angular擁有許多功能,讓我們更加容易地測試我們的應用。我們應該沒有借口不去寫測試(這個嘛……)。
一、 It is all about NOT mixing concerns(全部都關於避免代碼關係變得複雜……)
單元測試,正如名稱那樣,是關於測試單個“單元”的代碼。單元測試努力解答這些問題:我對邏輯的考慮是否已經正確?排序方法得出的結果是否正確?為瞭解答這些問題,將這些問題獨立出來顯得尤其重要。這是因為當我們在測試排序方法的時候,我們不想關心其他相關的片段,例如DOM元素或者發起XHR請求擷取資料等。明顯地,通常比較難做到在典型的項目中單獨調用一個函數。導致這個問題的原因是,開發人員通常把關係弄得很複雜,最終讓一個程式碼片段看起來可以做所有事情。它通過XHR擷取資料,對資料進行排序,然後操縱DOM。與angular一起,我們可以更加容易地寫出較好的代碼,所以angular為我們提供XHR(我們可以類比它)的依賴注入,angular還建立允許我們對model進行排序而不需要操作DOM的抽象。所以,到最後,我們可以簡單地寫一個排序方法,然後通過測試案例建立資料集合,供排序方法測試時使用,然後判斷結果model是否符合預期。測試無須等待XHR、者建立對應的DOM和判斷函數是否正確操作DOM。angular的核心思想包含代碼的可測試性,但同時也要求我們去做正確的事情。angular致力於簡化做正確事情的方法,但angular不是魔法,這意味著我們如果不遵循以下的幾點,我們最終可能會得出一個不可測試的應用。
1. Dependency Inject
有許多辦法可以獲得依賴的資源:1)我們可以使用new操作符;2)我們使用一個眾所周知的方式,被稱為” 全域單例”;3)我們可以向registry service請求(但我們如何取得一個registry?可以查看後面的章節);4)我們可以期待它會被傳遞過來。
上面列出的方法中,只有最後一個是可測試的,讓我們看看為什麼:
1) Using the new operator
使用new操作符時基本上沒有錯誤,但問題是通過new調用建構函式將會永久地將調用方與type綁定起來。舉個例子,我們嘗試執行個體化一個XHR對象,以讓我們可以從伺服器獲得一些資料。
function MyClass() { this.doWork = function() { var xhr = new XRH(); xhr.open(method,url,true); xhr.onreadystatechange = function() {…}; xhr.send(); }}
問題來了,在測試時,我們通常需要執行個體化一個可以返回測試資料或者網路錯誤的虛擬XHR。通過調用new XHR(),我們永久地綁定了真實的XHR,並且沒有一個很好的方法去替代它。當然,有一個糟糕的補救辦法,有很多理由可以證明那是一個糟糕的想法:
var oldXHR = XHR;XHR = new MockXHR() {};myClass.doWork();//判斷MockXHR是否通過正常的參數進行調用XHR = oldXHR;//如果忘了這一步,很容易會發生悲催的事情。
2) Global look-up
解決問題的另外一個方法是在一個眾所周知的地方擷取依賴的資源。
function MyClass() { this.doWork = function() { global.xhr({…}); };}
沒有建立新依賴對象的執行個體的情況下,問題基本上與new一致,除了那個悲催的補丁以外,沒有一個很好的方法可以再測試時攔截global.xhr的調用。測試的最基本的問題是global變數需要改為調用虛擬方法而被修改。想進一步瞭解它的壞處,可以參觀這裡:http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/
上面的代碼比較難去測試,所以我們必須修改global state:
var oldXHR = global.xhr;global.xhr = function mockXHR(){…};var myClass = new MyClass();//判斷MockXHR是否通過正常的參數進行調用global.xhr = oldXHR;//如果忘了這一步,很容易會發生悲催的事情。
3) Service Registry
擁有一個包含所有service的registry的話,似乎可以解決問題,然後,在測試代碼中替換所需要的service。
function MyClass() { var serviceRegistry = ???; this.doWork = function() { var xhr = serviceRegistry.get(“xhr”); … };}
但是,serviceRegistry來自哪裡?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)(這裡後面的文字跟亂碼一樣……沒看懂)
根據這個方法,將上面的Class修改為如下的方式:
var oldServiceLocator = global.serviceLocator;global.serviceLocator.set('xhr', function mockXHR() {});var myClass = new MyClass();myClass.doWork();//判斷MockXHR是否通過正常的參數進行調用global.serviceLocator = oldServiceLocator; //如果忘了這一步,很容易會發生悲催的事情。
4) Passing in Dependencies
最後,依賴資源可以被傳入。
function MyClass(xhr) { this.doWork = function() { xhr({…}); };}
這個是首選的方式,因為代碼無須理會xhr是從哪來的,也不關心誰建立了傳進來的xhr。因此,類的建立者與類的使用者可以分開編碼,這將建立的責任從邏輯中分離出來,這就是依賴注入的概述。
這個class很容易測試,在測試中我們可以這樣寫:
function xhrMock(args) {…}var myClass = new MyClass(xhrMock);myClass.doWrok();//做一些判斷……通過這個測試代碼,我們可以意識到沒有任何全域變數被破壞。
angular附帶的dependency-injection(http://www.jb51.net/article/91775.htm),通過這種方式編寫的代碼,更加容易編寫測試代碼,如果我們想編寫可測試性強的代碼,我們最好使用它。
2. Controllers
邏輯使每一個應用都是唯一的,這就是我們想去測試的。如果我們的邏輯裡面混雜著DOM的操作,這將會跟下面的例子一樣難測試:
function PasswordController() { // 擷取DOM對象的引用 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); }}
上面的代碼在測試時會遇到問題,因為它需要我們的執行測試時候,需要有正確的DOM。測試代碼會如下:
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('');
在angular中,controller嚴格地將DOM操作邏輯分離出來,將大大降低編寫測試案例的難度,看看下面的例子:
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'; } };}
測試代碼直截了當:
var pc = new PasswordController($scope);pc.password('abc');pc.grade();expect($scope.strength).toEqual('weak');
值得注意的是,測試代碼不僅僅更加間斷,而且更加容易追蹤。我們一直說測試案例是在講故事,而不是判斷其他不相關的東西。
3. Filters
filter(http://docs.angularjs.org/api/ng.$filter)是用於將資料轉換為對方便使用的格式。它們很重要,因為它們將轉換格式的責任從應用邏輯中分離出來,進一步簡化了應用邏輯。
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
後續繼續更新相關文章,謝謝大家對本站的支援!