文档章节

[译]编写可测试的JavaScript代码

暗夜在火星
 暗夜在火星
发布于 2016/04/02 21:25
字数 2320
阅读 123
收藏 3

[译]编写可测试的JavaScript代码

/**
 * 谨献给可爱的小黑
 *
 * 原文出处: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浏览器应用的早期工作之一是侦听由终端用户触发的DOM事件, 然后通过运行一些业务逻辑和在页面上显示结果来向用户作出响应。很容易就在设置DOM事件侦听的地方编写一个 做很多事情的匿名函数。由此产生的问题是你现在不得不模拟DOM事件以便测试你的匿名函数。这会产生代码行数 和执行测试的时间这两方面的开销。
 

取而代之,应该是编写一个命名的函数并把它传递给事件处理器。这样的话你可以直接为命名的函数编写测试并且 无须费事去触发一个假的DOM事件。

这不仅仅可以应用到DOM。很多API,包括在浏览器和在Node中,都是围绕着启动和侦听事件或者等待其他待完成的 异步工作类型而设计的。经验法则是如果你正在编写大量匿名回调函数,那么你的代码是不易测试的。

// 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应用时有一些坑是很容易掉进去的。但这些技巧让我们又充满了希望, 并且记得经常保持代码尽可能简单以及尽可能是可工作的,我们可以保持很高的测试覆盖率以及很低的代码复杂度!


------------------------   


© 著作权归作者所有

共有 人打赏支持
暗夜在火星

暗夜在火星

粉丝 159
博文 170
码字总数 319671
作品 1
广州
程序员
私信 提问
[译] WebAssembly: How and why

如何在浏览器中运行原生代码,为什么要这样做,这样做对Javascript和Web开发的未来有何意义 在所有浏览器里面,都运行着js代码,它们被js引擎解析和执行。然而,js并无法最理想地处理所有任务...

Skandar-Ln
08/23
0
0
【译】为什么ReasonReact是编写React的最佳方式

原文地址:medium.freecodecamp.org/psst-heres-… 你是否使用 React 来构建用户界面?好吧,我也是。接下来你会了解为什么要使用 ReasonML 来写 React 应用。 React 是构建用户界面的一种很...

吃吃吃吃不胖
10/18
0
0
[译] JavaScript 是如何工作的:对比 WebAssembly + 为什么在某些场景下它比 JavaScript 更合适

原文地址:How JavaScript works: A comparison with WebAssembly + why in certain cases it’s better to use it over JavaScript 原文作者:Alexander Zlatkov 译文出自:掘金翻译计划 本......

stormluke
05/23
0
0
【译】JavaScript的工作原理:引擎,运行时和调用堆栈的概述

原文地址:https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf(需要翻墙) 随着javascript变得越来越流行,很多团队的技术栈都开始使用它,比如前端、后...

有一些叫做Web API的东西,它们是由浏览器提供的,比如DOM,AJAX,setTimeout等等。
10/19
0
0
「译」JavaScript 当中的代码嗅探

原文:Shim sniffing 译文:JavaScript 当中的代码嗅探 译者:yuezk 除非有特殊需要,否则不要试图扩展原生对象和原型(prototype): 除非这样做是值得的。例如,向一些旧的浏览器中添加一些...

justjavac
2013/04/08
174
0

没有更多内容

加载失败,请刷新页面

加载更多

PHP生成CSV之内部换行

当我们使用PHP将采集到的文件内容保存到csv文件时,往往需要将采集内容进行二次过滤处理才能得到需要的内容。比如网页中的换行符,空格符等等。 对于空格等处理起来都比较简单,这里我们单独...

豆花饭烧土豆
今天
2
0
使用 mjml 生成 thymeleaf 邮件框架模板

发邮件算是系统开发的一个基本需求了,不过搞邮件模板实在是件恶心事,估计搞过的同仁都有体会。 得支持多种客户端 支持响应式 疼彻心扉的 outlook 多数客户端只支持 inline 形式的 css 布局...

郁也风
今天
8
0
让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字

让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字: 作者:孙冬梅;以前读韩国前总统朴槿惠的著作《绝望锻炼了我》时,里面有一句话令我印象深刻,她说“在我最困难的时期,...

原创小博客
今天
4
0
JAVA-四元数类

public class Quaternion { private final double x0, x1, x2, x3; // 四元数构造函数 public Quaternion(double x0, double x1, double x2, double x3) { this.x0 = ......

Pulsar-V
今天
18
0
Xshell利用Xftp传输文件,使用pure-ftpd搭建ftp服务

Xftp传输文件 如果已经通过Xshell登录到服务器,此时可以使用快捷键ctrl+alt+f 打开Xftp并展示Xshell当前的目录,之后直接拖拽传输文件即可。 pure-ftpd搭建ftp服务 pure-ftpd要比vsftp简单,...

野雪球
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部