在谈到单元测试之前, 先让我们认识敏捷软件开发中的两大概念。
一. BDD 与 TDD
测试分为2大流派, 分别为:
1.1 BDD: 行为驱动开发
先做实现, 然后对实现做尽量完整的测试。 行为驱动开发强调 作为什么角色,想要什么功能, 以便收益什么 , 关注的是整个系统的最终实现是否和用户期望一致。
1.2 TDD: 测试驱动开发
强调测试驱动, 先写测试用例, 根据测试用例驱动开发, 对于开发细节有很好的把握。
TDD的一个典型流程演示如下图:
流程说明:
- 开发人员先写一些测试代码(图中绿色圆圈)
- 开发人员执行测试用例, 然后毫无疑问的这些测试用例失败了, 因为测试中提到的类和方法并没有实现
- 开发人员开始实现测试用例里提到的方法
- 开发人员写好了某个功能点, 幸运的话, 之前相对于的测试用例通过了
- 开发人员可以重构代码,并添加注释,完成后期工作
二. 什么是单元测试
单元测试是指软件中最小可测试单元进行检查验证, 也称为模块测试。在node.js 中通过是对某个函数, 模块 , API进行正确验证, 以保证代码的可用性。
2.1单元测试作用
单元测试是提高代码质量的一个必要手段,也能够最大限度地减少对旧有功能的最大破坏。
一个很普遍的现象是作为开发人员在写代码的过程中很容易陷入思维漏洞,而在写测试的时候往往会考虑各种情况,单元测试起到的主要作用在于:
- 验证代码的正确性 (对于各种可能的输入,一旦测试覆盖,都能明确它的输出)
- 判定代码的改动是否影响已确定的结果, 避免个人协作开发中修改代码的时出错。
- 对于 API升级, 可以很好地检查代码是否向下兼容
- 简化调试过程
2.2 单元测试必要性
对于一些前端公共库, 如 lodash , koa , 及 react 而言, 他们对代码健壮性和质量有着更高的要求, 单元测试则是必备条件了。 除了库开发, node.js 应用中单元测试也很重要, 尤其是在项目快速迭代的过程中, 每个测试用例都给应用的文档提供了一层保障。
2.2.1 哪些项目需要引入单元测试
- 公共依赖库 , sdk这种工具库, 必须引入单元测试
- 工程应用, 对外暴露的 api 和公共方法, 最好引入单元测试
2.2.2 程序中哪些地方需要引入单元测试
- 开发过程中, 单元测试应该来测试那些可能会出错的地方, 或是那些边界情况
- 维护过程中,单元测试应该跟着我们的bug report来走,每一个bug都应该有个UnitTest。于是程序员就会对自己的代码变更有两个自信,一是bug 被 fixed,二是相同的bug不会再次出现。
单元测试怎么做
node.js 测试用例中基本语法, 主要有以下4个基本语句:
describe
: 定义一个测试套件it
: 定义一个测试用例expect
:断言的判断条件toEqual
: 断言的比较结果
测试套件和测试用例关系示意图:
三.单元测试工具
前端测试工具很多,常见的测试工具大致可分为 断言库, 测试框架, 测试辅助工具, 测试覆盖率等几类。
- 断言库: 提供上述断言的语义化方法, 用于对参与测试的值做各种各样的判断。 这些语义化方法会返回测试的结果, 要么成功, 要么失败。
- 测试框架: 提供一些方便的语法来描述测试用例, 以及对用例进行分组。
- 测试覆盖率工具: 测试覆盖率工具是用于统计测试用例对代码的测试情况, 生成相应的报表。
- 测试辅助工具: 测试spy, 测试 mock 等辅助功能。
3.1 断言库
断言是单元测试框架中核心的部分, 一般断言返回值是 boolean 值, 还可以是比较,是否为空, 正则等, 断言失败会导致测试不通过,或报告错误信息。
3.1.1断言语句基本类型
断言语句一般有以下四种类型语句 (以expect 为例)
- 同等性断言
expect(sth).toEqual(value)
expect(sth).not.toEqual(value)
- 比较性断言
expect(sth).toBeGreaterThan(number)
expect(sth).toBeLessThanOrEqual(number)
- 类型性断言
expect(sth).toBeInstanceOf(Class)
- 条件性测试
expect(sth).toBeTruthy()
expect(sth).toBeFalsy()
expect(sth).toBeDefined()
下面介绍4种常用的断言库
- assert: Node 原生支持的断言模块 支持 TDD
assert模块是 node 中大多数单元测试的基础, 很多第三方测试框架都用了 assert 模块, 甚至没有测试框架也可以用它做测试。
assert.ok(add(1,1));
assert.equal(add(1,1), 2);
- should: 支持BDD
(add(1, 1)).should.be.a.Number();
(add(1, 1)).should.equal(2);
- expect: 支持 BDD
expect(add(1, 1)).to.be.a("number")
expect(add(1, 1)).to.equal(2);
- chai: 可支持浏览器及 node的断言库, 支持 TDD与 BDD
should.js和expect.js相较于assert语义性更强,且支持类型检测,而should.js在语法上更加简明,同时.and支持链式语法。
3.2 测试框架
一个测试用例包含一个断言或多个断言, 那如何组织多个测试用例呢?
这时就需要测试框架了, 通过框架, 能够对测试用例进行分组测试, 并产出测试报告。
下面介绍下常见的4种测试框架
3.2.1 mocha 官方文档
特点:
- 功能非常丰富, 支持 BDD, TDD
- 支持运行在 node.js 和浏览器中
- 对异步测试支持非常友好
- 支持 4种hook, 包括 before/after/beforeEach/afterEach
// 引入需测试的模块或类
const add = require("./add");
// assert: nodejs内置断言模块
const assert = require("assert");
// describe:定义一组测试
describe("加法函数测试", function() {
before(function() {
// runs before all tests in this block
});
// it: 定义一个测试用例
it("1 加 1 应该等于 2", function() {
assert.equal(add(1, 1), 2);
});
after(function() {
// runs after all test in this block
});
});
3.2.2 Jasmine
Jasmine 是一个功能全面的测试框架, 内置断言 expect; 但是有全局声明, 且需要配置, 相对来说使用更复杂, 不够灵活。
const add = require("../src/add");
describe("加法函数测试", function () {
it("1加1等于2", function() {
expect(add(1, 1)).toEqual(2);
});
it("输出数字", function() {
expect(add(1, 1)).toEqual(jasmine.any(Number));
});
});
3.2.3 ava
虽然 javaScript 是单线程, 但在 node.js 里由于其异步的特性使得 IO可以并行。 AVA 利用这个优点让你的测试可以并发执行, 这对于IO繁重的测试特别有用, 特点如下:
- 轻量, 高效, 简单。
- 并发测试, 强制编写原子测试
- 没有隐藏的全局变量, 每个测试文件独立环境
- 支持 es7, promise, Generator, async, observable
- 内置断言, 强化断言信息
import test from 'ava';
function trimAll(string) {
return string.replace(/[\s\b]/g, '');
}
test('trimAll testing', t => {
// 字符串内含有空格符、制表符等空字符都应删除
t.is(trimAll(' \n \r \t \v \b \f B a r r i o r \n \r \t \v \b \f '), 'Barrior');
// 无空字符时,输出值应为输入值
t.is(trimAll('Barrior'), 'Barrior');
// 输入 new String 对象应与输入基本类型字符串结果相同
t.is(trimAll(new String(' T o m ')), 'Tom');
// 输入其他非字符串数据类型时,应抛出错误
[undefined, null, 0, true, [], {}, () => {}, Symbol()].forEach(type => {
t.throws(() => {
trimAll(type);
});
});
});
test(): 执行一个测试, 第一个参数为标题, 第二个参数为用例函数, 接收一个包含内置断言 API的参数 t, 也是唯一一个参数; 依照惯例该参数名字叫做 t , 没有必要重新取名。
3.2.4目前最流行的前端测试框架 官方文档
一个功能全面的'零配置'测试框架,几乎国内所有的大型互联网公司都在使用
jest是 facebook 出品的一个测试框架, 相对于其他测试框架, 最大的特点就是内置了常用的测试工具, 比如 自带断言expect
, 测试覆盖率工具
, ui测试工具
, mock能力
等, 同时可以收集成很多插件, 与主流的软件库 (vscode)配合测试, 比如: TypeScript, React, Vue 等, 真正实现了开箱即用。
jest 相比 mocha有更为清晰的说明文档, 并且和 ava 一样支持并行测试提供效率
jest 对异步测试支持很好,使用实例
// fetchData.test.js
import { fetchData } from './fetchData'
test('fetchData 返回结果为 { success: true }', async () => {
// fetchData().then(res => console.log(res))
await expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
})})
test('fetchData 返回结果为 404', async () => {
await expect(fetchData()).rejects.toThrow(
'Request failed with status code 404'
)})
test('fetchData 返回结果为 { success: true }', async () => {
const response = await fetchData()
expect(response.data).toEqual({
success: true
})})
test('fetchData 返回结果为 404', async () => {
expect.assertions(1) // 强制执行 catch 里的 expect
try {
await fetchData()
} catch (e) {
console.log(e.toString()) // Error: Request failed with status code 404
expect(e.toString()).toEqual('Error: Request failed with status code 404')
}})
3.3 HTTP请求模拟
在用node 做web开发的时候, 模拟http 请求时必不可少,如果都需要用浏览器来实现请求, 那就太low了。
- superTest
superTest 是一个非常棒的适用于node.js 的模拟Http 请求的库, 它封装了发送了http请求的接口, 并且提供了简单的 expect 断言来判断接口返回结果。
koa源码中也应用到了该库
const request = require('supertest');
const assert = require('assert');
const Koa = require('koa');
describe('app.request', () => {
const app1 = new Koa();
app1.request.message = 'hello';
const app2 = new Koa();
it('should merge properties', () => {
app1.use((ctx, next) => {
assert.equal(ctx.request.message, 'hello');
ctx.state = 204;
});
return request(app1.listen())
.get('/')
.expect(404)
})
it('should not affect the original prototype', () => {
app2.use((ctx, next) => {
assert.equal(ctx.request.message, undefined);
ctx.state = 204;
})
return request(app2.listen())
.get('/')
.expect(404)
})
})
3.4 测试辅助工具
node.js 应用中,代码与代码之间,模块与模块之间总是会存在着相互引用。 然而在单元测试中, 我们可能并不需要关系内部调用的方法的执行和结果, 只想知道它是否被正确调用即可, 甚至会指定该函数的返回值。 此时,使用 mock 函数就十分有必要。
mock 函数提供的以下三种特性, 在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现
举个例子
// math.js
export const getAResult = () => {
// a logic here
};
export const getBResult = () => {
// b logic here
};
// caculate.js
import { getAResult, getBResult } from "./math";
export const getABResult = () => getAResult() + getBResult();
在这里, getAResult() 和 getBResult() 就是 getABResult 这个函数的依赖。 如果我们关注这点是 getABResult 这个函数,我们就应该把 getAResult 和 getBResult mock掉, 剥离这种依赖。
- Sinon 测试辅助库
Sinon通过创建 测试替身 , 将我们代码中依赖的一些函数或者类, 替换成测试替身。
Sinon 有主要三个方法辅助我们进行测试: spy, stub, mock, 下面对这三个方法做下介绍:
- spy,可以提供函数调用的信息, 但不会改变函数的行为
通过对监视的函数进行包装,可以通过它清楚的知道该函数被调用过几次,传入什么参数,返回什么结果, 甚至是抛出的异常情况。
- Stub, 提供函数的调用信息,并且可以像实例一样, 让被 stubbed 的函数返回任何我们需要的行为。
一个stub可以使用最少的依赖方法来模拟该单元测试。 比如一个方法可能依赖另一个方法的执行, 而后者对我们来说是透明的, 好的做法是使用 stub对它进行隔离
var myObj = {
prop: function() {
return 'foo';
}
};
sinon.stub(myObj, 'prop').callsFake(function() {
return 'bar';
});
myObj.prop(); // 'bar'
四,实施单元测试准则
本篇文章的测试例子都很简单。然而,在实际工作中,单元测试是一个很头疼的事情。修改了代码有时候意味着必须修改单元测试, 写了新的函数或者API就得写新的单元测试。 实际上, 想通过测试消除所有bug,是不现实的。
总结
最后对本文内容做一个简单总结:
- TDD/BDD只是一种范式指导, 不应该成为应用开发中的一种限制
- 测试框架 第一选择jest
- mocha作为老牌框架,覆盖场景广,社区成熟度高,【mocha+chai+istanbul 】的组合也是一个不错的选择。
- 接口单元测试,http请求推荐supertest。
- 为了应用开发和维护的稳定性,需要合理设计单元测试规范,编写测试用例
- 软件工程的质量不是测试出来的,而是设计和维护出来的。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!