作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
约书亚·莫克的头像

约书亚模拟

Joshua是一名前端架构师和web应用程序开发人员. 他在构建交互式web应用程序方面有15年的经验.

以前在

竞选班长
分享

我们是否使用 节点 与像Mocha或Jasmine这样的测试框架配对, 或者在像PhantomJS这样的无头浏览器中运行依赖dom的测试, 我们对JavaScript单元测试的选择比以往任何时候都好.

然而,这并不意味着我们正在测试的代码对我们来说就像我们的工具一样容易! 组织和编写易于测试的代码需要一些努力和计划, 但也有一些规律, 受到函数式编程概念的启发, 我们可以使用它来避免在测试代码时陷入困境. 在本文中, 我们将介绍一些用JavaScript编写可测试代码的有用技巧和模式.

保持业务逻辑和显示逻辑分开

基于javascript的浏览器应用程序的主要工作之一是监听 DOM事件 由终端用户触发, 然后通过运行一些业务逻辑和在页面上显示结果来响应它们. 编写一个匿名函数来完成设置DOM事件侦听器的大部分工作是很有诱惑力的. 这造成的问题是,现在必须模拟DOM事件来测试匿名函数. 这会在代码行和测试运行时间上产生开销.

相反,应该编写一个命名函数并将其传递给事件处理程序. 这样,您就可以直接为命名函数编写测试,而不必费力去触发一个虚假的DOM事件.

这不仅适用于DOM. 许多api, 在浏览器和节点中, 是围绕触发和监听事件还是等待其他类型的异步工作完成而设计的. 经验法则是,如果您正在编写大量匿名回调函数, 您的代码可能不容易测试.

//很难测试
$('按钮').on('click', () => {
    $.getJSON(/道路/ /数据)
        .then(data => {
            $(“#我的清单”).Html ('results: ' + data . Html.加入(','));
        });
});

// testable; we can directly run fetchThings to see if it
//发出一个AJAX请求,而不必触发DOM
//事件,我们可以直接运行showThings来查看
//在DOM中显示数据而不进行AJAX请求
$('按钮').on('click', () => fetchThings(showThings));

函数fetchThings(callback) {
    $.getJSON(/道路/ /数据).然后(回调);
}

showThings(data) {
    $(“#我的清单”).Html ('results: ' + data . Html.加入(','));
}

对异步代码使用回调或承诺

在上面的代码示例中,我们重构的fetchThings函数运行一个 AJAX 请求,它以异步方式完成其大部分工作. 这意味着我们不能运行函数并测试它是否完成了我们所期望的一切, 因为我们不知道它什么时候结束运行.

解决此问题的最常见方法是将回调函数作为参数传递给异步运行的函数. 在单元测试中,您可以在通过的回调中运行断言.

插图:在单元测试中使用回调函数作为参数

另一种常见且日益流行的组织异步代码的方法是使用Promise API. 幸运的是,美元.ajax和大多数其他jQuery的异步函数已经返回了一个Promise对象, 所以已经涵盖了很多常见的用例.

//很难测试; we don't know how long the AJAX request will run
函数fetchData() {
    $.Ajax ({url: '/path/to/data'});
}

// testable; we can pass a callback and run assertions inside it
函数fetchDataWithCallback(回调){
    $.ajax ({
        url: /道路/ /数据,
        成功:回调,
    });
}

// also testable; we can run assertions when the returned Promise resolves
函数fetchDataWithPromise() {
    返回$.Ajax ({url: '/path/to/data'});
}

避免副作用

编写接受参数并仅基于这些参数返回值的函数, 就像在数学方程式中输入数字来得到结果一样. 如果您的函数依赖于某些外部状态(类实例的属性或文件的内容), 例如), 你必须在测试你的功能之前设置那个状态, 你必须在测试中做更多的准备工作. 您必须相信正在运行的任何其他代码都不会改变相同的状态.

插图:由外部状态引起的级联效应.

同样的道理, 避免在运行时编写改变外部状态的函数(如写入文件或将值保存到数据库). 这可以防止可能影响您自信地测试其他代码的能力的副作用. 在一般情况下, 最好让副作用尽可能靠近代码的边缘, “表面积”越小越好. 对于类和对象实例, 类方法的副作用应该限制在被测试的类实例的状态内.

//很难测试; we have to set up a globalListOfCars object and set up a
//使用#list-of-models节点来测试这段代码
processCarData() {
    const models = globalListOfCars.map(car => car.模型);
    $ (' # list-of-models ').html(模型.加入(','));
}

//易于测试; we can pass an argument and test its return value, without
//在窗口上设置任何全局值或检查DOM的结果
函数buildModelsString(cars) {
    Const models = cars.map(car => car.模型);
    回归模型.加入(" ");
}

使用依赖注入

减少函数使用外部状态的一种常见模式是依赖注入——将函数的所有外部需求作为函数参数传递.

// depends on an external state database connector instance; hard to test
函数updateRow(rowId, data) {
    myGlobalDatabaseConnector.更新(rowId数据);
}

// takes a database connector instance in as an argument; easy to test!
函数updateRow(rowId, data, databaseConnector) {
    databaseConnector.更新(rowId数据);
}

使用依赖注入的一个主要好处是,您可以从单元测试中传入mock对象,这些对象不会产生真正的副作用(在本例中是这样), 更新数据库行),您可以断言您的模拟对象以预期的方式进行了操作.

给每个功能一个单一的目的

将执行多项任务的长函数分解为一系列短函数, 专用 功能. 这使得测试每个函数是否正确地完成了它的部分要容易得多, 而不是在返回值之前希望一个大的函数做的一切都是正确的.

在函数式编程中, 将几个单一用途的函数串在一起的行为称为组合. 下划线.Js甚至有一个函数 _.组成, 这需要一个函数列表并将它们链接在一起, 获取每一步的返回值并将其传递给下一个函数.

//很难测试
函数createGreeting(名字, 位置, 年龄) {
    让问候;
    if (位置 === 'Mexico') {
        问候= '!你好';
    } else {
        问候= 'Hello';
    }

    问候+= ' ' +姓名.toUpperCase() + '! ';

    问候+= 'You are ' + 年龄 + ' years old.';

    返回的问候;
}

//易于测试
getBeginning(位置) {
    if (位置 === 'Mexico') {
        返回“¡你好”;
    } else {
        返回“你好”;
    }
}

getMiddle(name) {
    返回' ' +名称.toUpperCase() + '! ';
}

gettend (年龄) {
    返回'You are ' + 年龄 + ' years old '.';
}

函数createGreeting(名字, 位置, 年龄) {
    返回getBeginning(位置) + getMiddle(name) + getEnd(年龄);
}

不要改变参数

In JavaScript数组和对象是通过引用而不是值传递的,并且它们是可变的. 这意味着当您将对象或数组作为参数传递给函数时, 你的代码和你传递给对象或数组的函数都有能力改变内存中该数组或对象的相同实例. 这意味着如果您正在测试自己的代码, 您必须相信代码调用的所有函数都不会改变对象. 每次在代码中添加更改同一对象的新位置时, 要想知道那个物体应该是什么样子越来越难了, 让测试变得更加困难.

插图:参数的变化可能会导致问题

而不是, 如果你有一个接受一个对象或数组的函数,让它作用于那个对象或数组,就好像它是只读的一样. 在代码中创建新对象或数组,并根据需要向其添加值. 或者,使用 下划线 或Lodash在对其进行操作之前克隆传递的对象或数组. 使用像Immutable这样的工具会更好.Js创建只读数据结构.

//修改传递给它的对象
函数upperCaseLocation(customerInfo) {
    customerInfo.位置 = customerInfo.位置.toUpperCase ();
    返回customerInfo;
}

//返回一个新对象
函数upperCaseLocation(customerInfo) {
    返回{
        名称:customerInfo.名字,
        地点:customerInfo.位置.toUpperCase (),
        年龄:customerInfo.年龄
    };
}

先写测试再写代码

在被测试的代码被调用之前编写单元测试的过程 测试驱动开发 (TDD). 许多开发人员发现TDD非常有帮助.

首先编写测试, 你不得不从使用它的开发人员的角度来考虑你所公开的API. 它还有助于确保您只编写足够的代码来满足测试所强制执行的契约, 而不是过度设计一个不必要的复杂的解决方案.

在实践中,TDD是一种很难对所有代码更改进行提交的规则. 但是当它看起来值得一试时,它是保证您保持所有代码可测试性的好方法.

总结

我们都知道,在编写和测试复杂的JavaScript应用程序时,很容易陷入一些陷阱. 但希望有了这些建议, 并记住始终保持我们的代码尽可能简单和实用, 我们可以保持较高的测试覆盖率和较低的代码复杂度!

就这一主题咨询作者或专家.
预约电话
约书亚·莫克的头像
约书亚模拟

位于 美国田纳西州纳什维尔

成员自 2016年1月5日

作者简介

Joshua是一名前端架构师和web应用程序开发人员. 他在构建交互式web应用程序方面有15年的经验.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

以前在

竞选班长

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.