/** * 謹獻給可愛的小黑 * * 原文出處:https://www.toptal.com/javascript/writing-testable-code-in-javascript * @author dogstar.huang <chanzonghuang@gmail.com> 2016-04-02 */
無論咱們正是使用的是像Mocha或Jasmine這樣結點配對的測試框架,或者是像PhantomJS這樣模擬瀏覽器圍繞DOM 依賴的測試,如今咱們對於JavaScript單元測試的選擇都比之前好了不少。javascript
然而,這並不意味着咱們要測試的代碼如同咱們的工具那樣容易!組織和編寫易於測試的代碼須要一些努力和計 劃,但這裏有一些由函數編程概念啓發的模式,可用於當須要測試代碼時避免咱們陷入痛苦之中。在這篇文章中, 咱們將探索一些用於編寫可測試的JavaScript代碼的有用技巧與模式。html
基於JavaScript瀏覽器應用的早期工做之一是偵聽由終端用戶觸發的DOM事件, 而後經過運行一些業務邏輯和在頁面上顯示結果來向用戶做出響應。很容易就在設置DOM事件偵聽的地方編寫一個 作不少事情的匿名函數。由此產生的問題是你如今不得不模擬DOM事件以便測試你的匿名函數。這會產生代碼行數 和執行測試的時間這兩方面的開銷。
java
取而代之,應該是編寫一個命名的函數並把它傳遞給事件處理器。這樣的話你能夠直接爲命名的函數編寫測試而且 無須費事去觸發一個假的DOM事件。node
這不只僅能夠應用到DOM。不少API,包括在瀏覽器和在Node中,都是圍繞着啓動和偵聽事件或者等待其餘待完成的 異步工做類型而設計的。經驗法則是若是你正在編寫大量匿名回調函數,那麼你的代碼是不易測試的。ajax
// hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }
在上面的示例中,咱們重構後的refactored方法執行了一個AJAX執行, 以便異步完成它大部分的工做。這意味着咱們不能執行這個方法以及測試咱們指望它所作的全部事情,由於咱們 不知道它什麼時候完成。數據庫
解決這個問題最多見的方式是把一個回調函數做爲一個參數傳遞給這個異步執行的方法。在你的單元測試裏能夠 在所傳遞的回調中執行你的斷言。
編程
另一個通用且日漸流行的組織異步代碼的方式是使用承諾API(Promise API)。幸運的是,$.ajax和其餘大多 數的jQuery異步方法已經返回了一個Promise對象,因此已經能夠覆蓋到大量通用的狀況。數組
// hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }
編寫接收參數而且返回一個基於獨自這些參數的返回的函數,就像是把數字衝壓到一條數據公式而後獲得一個結果。 若是你的函數依賴於一些額外的狀態(例如某個類實例的屬性或者某個文件的內容),而且你須要在測試你的函數 前設置好這些狀態的話,你不得不在測試中作更多的啓動工做。你得相信任何其餘正在運行的代碼不會修改相同的 狀態。
瀏覽器
本着一樣的精神,避免編寫當運行時會修改外部狀態(如文件寫入或者保存數據到數據庫)的函數。這樣可防止 可能影響到你自信地測試其餘代碼的能力的反作用。一般來說,最好是儘量地保持反作用靠近你的代碼邊緣, 儘量地少「表面積」。在類和對象實例中,類方法的反作用應該被限制在正在被測試的類實例的狀態。框架
// hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }
對於減小函數對外部狀態的使用的通用模式是依賴注入 -- 將函數所有的額外須要做爲函數參數傳遞。
// depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }
使用依賴注入只要的一個好處是你能夠傳遞來自單元測試而不會產生實際反作用(在這裏是更新數據庫的紀錄) 的模擬對象而且你能夠斷言模擬對象是否定期望的方式工做。
把長長的作了若干件事情的函數分割成一系列簡短、單一職責的函數。 這使得相比於但願一個巨大的函數在返回一個值前正確地作所有事情,測試每一個小函數正確作好各自那部分要遠簡 單得多。
在功能編程裏,把若干個單一職責的函數串在一塊兒的行爲叫作組合。Underscore.js甚至有一個_.compose
函數, 能夠接收一個函數列表而且把他們鏈在一塊兒,接收每一步返回的值而且把它傳遞給下一行的函數。
// hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return '¡Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }
在JavaScript裏,數組和對象是經過按引用而不是按值傳值,而且他們 是能夠被修改的。這意味着當你把一個對象或者一個數組做爲參數傳遞給一個函數時,你的代碼和傳遞對象或數組 的函數都有能力修改在內存中相同的數組或對象實例。這意味着若是你正在測試本身的代碼,你不得不相信你的代 碼所調用的所有函數都沒有修改你的對象。每一次添加一處修改相同對象的代碼,都逐漸使得追蹤對象的看起來是 怎樣變動愈來愈困難,使得測試更難。
相反地,若是你有一個接收了對象或者數組的函數並根據這個對象或數組採起行動的話,就假設它是隻讀的。在代 碼中建立一個新的對象或數組而且根據你的須要爲其添加值。或者,在操做它以前使用Underscore或者Lodash克隆傳遞的對象或者數組。甚至更進一步,使用像Immutable.js這樣的工具建立只讀的數組結構。
// alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }
在寫待測試的代碼先寫單元測試的過程稱爲測試驅動開發(TDD)。 大量開發人員發現TDD頗有幫助。
經過先寫測試,強制你從消費它的開發人員的視角來考慮暴露的API。它也有助於確保你只編寫恰到好處的代碼來 知足你的測試強制的契約,而不是過分設計一個不必的複雜的解決方案。
在實踐中,TDD要用於所有代碼的變化是很困難的。但當它看起來值得嘗試時,它是一個保證你正保持所有的代碼 都是可測試的不錯的方式。
咱們都知道當編寫和測試複雜JavaScript應用時有一些坑是很容易掉進去的。但這些技巧讓咱們又充滿了但願, 而且記得常常保持代碼儘量簡單以及儘量是可工做的,咱們能夠保持很高的測試覆蓋率以及很低的代碼複雜度!
------------------------
本做品採用知識共享署名-非商業性使用-相同方式共享 3.0 未本地化版本許可協議進行許可。
本文翻譯做者爲:dogstar,發表於艾翻譯(itran.cc);歡迎轉載,但請註明出處,謝謝!