文档章节

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

暗夜在火星
 暗夜在火星
发布于 2016/04/02 21:25
字数 2320
阅读 114
收藏 3
点赞 1
评论 0

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


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


© 著作权归作者所有

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

暗夜在火星

粉丝 150
博文 160
码字总数 310930
作品 1
广州
程序员
[译] 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 是如何工作的:CSS 和 JS 动画背后的原理 + 如何优化性能

原文地址:How JavaScript works: Under the hood of CSS and JS animations + how to optimize their performance 原文作者:Alexander Zlatkov 译文出自:掘金翻译计划 本文永久链接:git......

辣手摧花
05/15
0
0
[译] 论 Rust 和 WebAssembly 对源码地址索引的极限优化

原文地址:Oxidizing Source Maps with Rust and WebAssembly 原文作者:Nick Fitzgerald 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m… 译者:D-kylin Tom Tromey 和我尝...

LeviDing
前天
0
0
为什么JavaScript是你应当学习的下一个(或第一个)编程语言

原文出处:dentedreality 译文出处:腊八粥 本文是翻译,版权归原作者所有 我已经被多次问到,如果我刚刚开始,我应当学习什么编程语言,答案一定是JavaScript,下面是为什么: 最简单的开发...

dentedreality
2014/12/02
0
0
50个实用的JavaScript工具

JavaScript是一个功能强大的客户端脚本语言,许多现代化的网站和Web应用程序都会使用到它。JavaScript可以增强用户的体验,并提供丰富的互动式组件和功能。虽然它的语法相当简单,但是对开发...

晨曦之光
2012/03/09
0
0
50个实用的JavaScript工具

【IT168 分析评论】JavaScript是一个功能强大的客户端脚本语言,许多现代化的网站和Web应用程序都会使用到它。JavaScript可以增强用户的体验,并提供丰富的互动式组件和功能。虽然它的语法相...

晨曦之光
2012/03/09
0
0
CoffeeScript?TypeScript?还是JavaScript?

原文地址(source): http://innoarchitech.com/coffeescript-typescript-javascript/ 请注意本文只是我的偏见,我努力地理解借助CoffeeScript或TypeScript之类的编译器写JavaScript代码的理...

第三方支付接口
2014/11/30
0
0
[译] JavaScript 如何工作:对引擎、运行时、调用堆栈的概述

原文地址: https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf PS: 好久没写东西了,最近一直在准备写一个自己的博客,最后一些技术方向已经敲定了,又可...

小烜同学
2017/11/12
0
0
JavaScript语言基础-环境搭建

我们要想编写和运行JavaScript脚本,则需要:JavaScript编辑工具和JavaScript运行测试环境。下面我们分别介绍一下。 JavaScript编辑工具 JavaScript编辑工具最简单的可以使用一些文本编辑工具...

智捷课堂
2015/03/12
0
0
JavaScript 测试及校验工具

JavaScript 是一款强大的广泛运用于现代Web站点及应用的脚本语言。作为一个技艺精湛的 Web 开发者,掌握JavaScript可以增强用户的使用体验,提供交互及富客户端等功能。 尽管JavaScript 的语...

晨曦之光
2012/03/09
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

expect(spawn) 自动化git提交和scp拷贝---centos(linux)

**在进行SCP文件拷贝中,往往需要进行用户密码的输入,即用户交互。若采用自动化脚本的方式进行,则可用以下方式: ** #!/usr/bin/expect #设置参数 set src [lindex $argv 0] set dest [lin...

helplove
6分钟前
1
0
用Build来构建对象的写法

如果一个类的属性过多,用构造器来构建对象很难写,因此我们时用Build方式来构建对象。写法大致如下。 import java.io.Serializable;import java.util.Date;public class Log impleme...

算法之名
8分钟前
11
0
利用 acme.sh 获取网站证书并配置https访问

acme.sh 实现了 acme 协议, 可以从 letsencrypt 生成免费的证书.(https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E) 主要步骤: 安装 acme.sh 生成证书 copy 证书到 nginx/ap...

haoyuehong
21分钟前
2
0
微擎框架内如何根据media_id获取到微信图片的路径

微擎的框架内,图片选择后,获取的是那个字符串是media_id,相当于你这张图片在微信的图片服务器里面的id 要求是:获取https://mmbiz.qpic.cn/mmbiz_jpg/…… 微信图片的路径 而微信并没有根据m...

老bia同学
25分钟前
1
0
Spring boot中日期的json格式化

Model 在model层中,类的日期属性上面添加如下注解: @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") 参考 Jackson Date格式化教程...

亚林瓜子
27分钟前
2
0
Eclipse:Failed to load the JNI shared library

1.问题背景: 由于我之前使用jdk1.9学习,当使用Luke的时候发现jdk版本过高,需要向下配置jdk,就向朋友拷了一个安装包。重新配置路径后,便开始报错。 2.问题描述: Failed to load the JNI...

tinder_boy
29分钟前
1
0
少儿学习编程课程是否真的适合七八岁的低龄儿童[图]

少儿学习编程课程是否真的适合七八岁的低龄儿童[图]: 天下熙熙皆为利来,天下攘攘皆为利往。 这几年来,乐高教育机构在国内如同雨后春笋般出现,当然关闭/转手的也很多。从教师角度来看,部...

原创小博客
35分钟前
1
0
ES12-词项查询

1.词项查询介绍 全文查询将在执行之前分析查询字符串,但词项级别查询将按照存储在倒排索引中的词项进行精确操作。这些查询通常用于数字,日期和枚举等结构化数据,而不是全文本字段。 或者,...

贾峰uk
43分钟前
2
0
http状态码与ajax的状态值

ajax状态值 1.1 200 & OK:状态请求成功

litCabbage
46分钟前
2
0
iOS动画效果合集、飞吧企鹅游戏、换肤方案、画板、文字效果等源码

iOS精选源码 动画知识运用及常见动画效果收集 3D卡片拖拽卡片叠加卡片 iFIERO - FLYING PENGUIN 飞吧企鹅SpriteKit游戏(源码) Swift封装的空数据提醒界面EmptyView 沙盒文件浏览与分享调试控...

sunnyaigd
49分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部