/** * 谨献给可爱的小黑 * * 原文出处: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);欢迎转载,但请注明出处,谢谢!