如果你听说过“测试驱动开发”(TDD:Test-Driven Development),单元测试就不陌生。
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs(),我们可以编写出以下几个测试用例:
输入正数,比如1、1.2、0.99,期待返回值与输入相同;
输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;
输入0,期待返回0;
输入非数值类型,比如null、[]、{},期待抛出Error。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。
单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。
这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
mocha
mocha是JavaScript的一种单元测试框架,既可以在浏览器环境下运行,也可以在Node.js环境下运行。
使用mocha,我们就只需要专注于编写单元测试本身,然后,让mocha去自动运行所有的测试,并给出测试结果。
mocha的特点主要有:
-
既可以测试简单的JavaScript函数,又可以测试异步代码,因为异步是JavaScript的特性之一;
-
可以自动运行所有测试,也可以只运行特定的测试;
-
可以支持before、after、beforeEach和afterEach来编写初始化代码。
假设我们编写了一个hello.js
,并且输出一个简单的求和函数:
// hello.js
module.exports = function (...rest) {
var sum = 0;
for (let n of rest) {
sum += n;
}
return sum;
};
这个函数非常简单,就是对输入的任意参数求和并返回结果。
如果我们想对这个函数进行测试,可以写一个test.js
,然后使用Node.js提供的assert
模块进行断言:
// test.js
const assert = require(‘assert‘);
const sum = require(‘./hello‘);
assert.strictEqual(sum(), 0);
assert.strictEqual(sum(1), 1);
assert.strictEqual(sum(1, 2), 3);
assert.strictEqual(sum(1, 2, 3), 6);
assert
模块非常简单,它断言一个表达式为true。如果断言失败,就抛出Error。可以在Node.js文档中查看assert
模块的所有API。
单独写一个test.js
的缺点是没法自动运行测试,而且,如果第一个assert报错,后面的测试也执行不了了。
如果有很多测试需要运行,就必须把这些测试全部组织起来,然后统一执行,并且得到执行结果。这就是我们为什么要用mocha来编写并运行测试。
mocha test
我们创建hello-test
工程来编写hello.js
以及相关测试。工程结构如下:
hello-test/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- hello.js <-- 待测试js文件
|
+- test/ <-- 存放所有test
| |
| +- hello-test.js <-- 测试文件
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
我们首先在package.json
中添加mocha的依赖包。和其他依赖包不同,这次我们并没有把依赖包添加到 "dependencies"
中,而是"devDependencies"
:
{
...
"dependencies": {},
"devDependencies": {
"mocha": "3.0.2"
}
}
如果一个模块在运行的时候并不需要,仅仅在开发时才需要,就可以放到devDependencies
中。这样,正式打包发布时,devDependencies
的包不会被包含进来。
然后使用npm install
安装。
注意,很多文章会让你用命令npm install -g mocha
把mocha安装到全局module中。这是不需要的。尽量不要安装全局模块,因为全局模块会影响到所有Node.js的工程。
紧接着,我们在test
目录下创建hello-test.js
来编写测试。
mocha默认会执行test
目录下的所有测试,不要去改变默认目录。
hello-test.js
内容如下:
const assert = require(‘assert‘);
const sum = require(‘../hello‘);
describe(‘#hello.js‘, () => {
describe(‘#sum()‘, () => {
it(‘sum() should return 0‘, () => {
assert.strictEqual(sum(), 0);
});
it(‘sum(1) should return 1‘, () => {
assert.strictEqual(sum(1), 1);
});
it(‘sum(1, 2) should return 3‘, () => {
assert.strictEqual(sum(1, 2), 3);
});
it(‘sum(1, 2, 3) should return 6‘, () => {
assert.strictEqual(sum(1, 2, 3), 6);
});
});
});
这里我们使用mocha默认的BDD-style的测试。describe
可以任意嵌套,以便把相关测试看成一组测试。
每个it("name", function() {...})
就代表一个测试。例如,为了测试sum(1, 2)
,我们这样写:
it(‘sum(1, 2) should return 3‘, () => {
assert.strictEqual(sum(1, 2), 3);
});
编写测试的原则是,一次只测一种情况,且测试代码要非常简单。我们编写多个测试来分别测试不同的输入,并使用assert
判断输出是否是我们所期望的。
运行测试
下一步,我们就可以用mocha运行测试了。
如何运行?有三种方法。
方法一,可以打开命令提示符,切换到hello-test
目录,然后执行命令:
C:\...\hello-test> node_modules\mocha\bin\mocha
mocha就会自动执行所有测试,然后输出如下:
#hello.js
#sum()
? sum() should return 0
? sum(1) should return 1
? sum(1, 2) should return 3
? sum(1, 2, 3) should return 6
4 passing (7ms)
这说明我们编写的4个测试全部通过。如果没有通过,要么修改测试代码,要么修改hello.js
,直到测试全部通过为止。
方法二,我们在package.json
中添加npm命令:
{
...
"scripts": {
"test": "mocha"
},
...
}
然后在hello-test
目录下执行命令:
C:\...\hello-test> npm test
可以得到和上面一样的输出。这种方式通过npm执行命令,输入的命令比较简单。
方法三,我们在VS Code中创建配置文件.vscode/launch.json
,然后编写两个配置选项:
{
"version": "0.2.0",
"configurations": [
{
"name": "Run",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/hello.js",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development"
},
"externalConsole": false,
"sourceMaps": false,
"outDir": null
},
{
"name": "Test",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/mocha/bin/mocha",
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "test"
},
"externalConsole": false,
"sourceMaps": false,
"outDir": null
}
]
}
注意第一个配置选项Run
是正常执行一个.js文件,第二个配置选项Test
我们填入"program": "${workspaceRoot}/node_modules/mocha/bin/mocha"
,并设置env
为"NODE_ENV": "test"
,这样,就可以在VS Code中打开Debug面板,选择Test
,运行,即可在Console面板中看到测试结果:
before和after
在测试前初始化资源,测试后释放资源是非常常见的。mocha提供了before、after、beforeEach和afterEach来实现这些功能。
我们把hello-test.js
改为:
const assert = require(‘assert‘);
const sum = require(‘../hello‘);
describe(‘#hello.js‘, () => {
describe(‘#sum()‘, () => {
before(function () {
console.log(‘before:‘);
});
after(function () {
console.log(‘after.‘);
});
beforeEach(function () {
console.log(‘ beforeEach:‘);
});
afterEach(function () {
console.log(‘ afterEach.‘);
});
it(‘sum() should return 0‘, () => {
assert.strictEqual(sum(), 0);
});
it(‘sum(1) should return 1‘, () => {
assert.strictEqual(sum(1), 1);
});
it(‘sum(1, 2) should return 3‘, () => {
assert.strictEqual(sum(1, 2), 3);
});
it(‘sum(1, 2, 3) should return 6‘, () => {
assert.strictEqual(sum(1, 2, 3), 6);
});
});
});
再次运行,可以看到每个test执行前后会分别执行beforeEach()
和afterEach()
,以及一组test执行前后会分别执行before()
和after()
:
#hello.js
#sum()
before:
beforeEach:
? sum() should return 0
afterEach.
beforeEach:
? sum(1) should return 1
afterEach.
beforeEach:
? sum(1, 2) should return 3
afterEach.
beforeEach:
? sum(1, 2, 3) should return 6
afterEach.
after.
4 passing (8ms)
用mocha测试一个函数是非常简单的,但是,在JavaScript的世界中,更多的时候,我们编写的是异步代码,所以,我们需要用mocha测试异步函数。
我们把上一节的hello-test
工程复制一份,重命名为async-test
,然后,把hello.js
改造为异步函数:
const fs = require(‘mz/fs‘);
// a simple async function:
module.exports = async () => {
let expression = await fs.readFile(‘./data.txt‘, ‘utf-8‘);
let fn = new Function(‘return ‘ + expression);
let r = fn();
console.log(`Calculate: ${expression} = ${r}`);
return r;
};
这个async函数通过读取data.txt
的内容获取表达式,这样它就变成了异步。我们编写一个data.txt
文件,内容如下:
1 + (2 + 4) * (9 - 2) / 3
别忘了在package.json
中添加依赖包:
"dependencies": {
"mz": "2.4.0"
},
紧接着,我们在test
目录中添加一个await-test.js
,测试hello.js
的async函数。
我们先看看mocha如何实现异步测试。
如果要测试同步函数,我们传入无参数函数即可:
it(‘test sync function‘, function () {
// TODO:
assert(true);
});
如果要测试异步函数,我们要传入的函数需要带一个参数,通常命名为done
:
it(‘test async function‘, function (done) {
fs.readFile(‘filepath‘, function (err, data) {
if (err) {
done(err);
} else {
done();
}
});
});
测试异步函数需要在函数内部手动调用done()
表示测试成功,done(err)
表示测试出错。
对于用ES7的async编写的函数,我们可以这么写:
it(‘#async with done‘, (done) => {
(async function () {
try {
let r = await hello();
assert.strictEqual(r, 15);
done();
} catch (err) {
done(err);
}
})();
});
但是用try...catch太麻烦。还有一种更简单的写法,就是直接把async函数当成同步函数来测试:
it(‘#async function‘, async () => {
let r = await hello();
assert.strictEqual(r, 15);
});
这么写异步测试,太简单了有木有!
我们把上一个hello-test
工程复制为async-test
,结构如下:
async-test/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- hello.js <-- 待测试js文件
|
+- data.txt <-- 数据文件
|
+- test/ <-- 存放所有test
| |
| +- await-test.js <-- 异步测试
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
现在,在命令行窗口运行命令node_modules\mocha\bin\mocha
,测试就可以正常执行:
#async hello
#asyncCalculate()
Calculate: 1 + (2 + 4) * (9 - 2) / 3 = 15
? #async function
1 passing (11ms)
第二种方法是在package.json
中把script
改为:
"scripts": {
"test": "mocha"
}
这样就可以在命令行窗口通过npm test
执行测试。
第三种方法是在VS Code配置文件中把program
改为:
"program": "${workspaceRoot}/node_modules/mocha/bin/mocha"
这样就可以在VS Code中直接运行测试。
编写异步代码时,我们要坚持使用async
和await
关键字,这样,编写测试也同样简单。
用mocha测试一个async函数是非常方便的。现在,当我们有了一个koa的Web应用程序时,我们怎么用mocha来自动化测试Web应用程序呢?
一个简单的想法就是在测试前启动koa的app,然后运行async测试,在测试代码中发送http请求,收到响应后检查结果,这样,一个基于http接口的测试就可以自动运行。
我们先创建一个最简单的koa应用,结构如下:
koa-test/
|
+- .vscode/
| |
| +- launch.json <-- VSCode 配置文件
|
+- app.js <-- koa app文件
|
+- start.js <-- app启动入口
|
+- test/ <-- 存放所有test
| |
| +- app-test.js <-- 异步测试
|
+- package.json <-- 项目描述文件
|
+- node_modules/ <-- npm安装的所有依赖包
这个koa应用和前面的koa应用稍有不同的是,app.js
只负责创建app
实例,并不监听端口:
// app.js
const Koa = require(‘koa‘);
const app = new Koa();
app.use(async (ctx, next) => {
const start = new Date().getTime();
await next();
const ms = new Date().getTime() - start;
console.log(`${ctx.request.method} ${ctx.request.url}: ${ms}ms`);
ctx.response.set(‘X-Response-Time‘, `${ms}ms`);
});
app.use(async (ctx, next) => {
var name = ctx.request.query.name || ‘world‘;
ctx.response.type = ‘text/html‘;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});
module.exports = app;
而start.js
负责真正启动应用:
// start.js
const app = require(‘./app‘);
app.listen(3000);
console.log(‘app started at port 3000...‘);
这样做的目的是便于后面的测试。
紧接着,我们在test
目录下创建app-test.js
,来测试这个koa应用。
在测试前,我们在package.json
中添加devDependencies
,除了mocha外,我们还需要一个简单而强大的测试模块supertest
:
{
...
"devDependencies": {
"mocha": "3.0.2",
"supertest": "3.0.0"
}
}
运行npm install
后,我们开始编写测试:
// app-test.js
const
request = require(‘supertest‘),
app = require(‘../app‘);
describe(‘#test koa app‘, () => {
let server = app.listen(9900);
describe(‘#test server‘, () => {
it(‘#test GET /‘, async () => {
let res = await request(server)
.get(‘/‘)
.expect(‘Content-Type‘, /text\/html/)
.expect(200, ‘<h1>Hello, world!</h1>‘);
});
it(‘#test GET /path?name=Bob‘, async () => {
let res = await request(server)
.get(‘/path?name=Bob‘)
.expect(‘Content-Type‘, /text\/html/)
.expect(200, ‘<h1>Hello, Bob!</h1>‘);
});
});
});
在测试中,我们首先导入supertest
模块,然后导入app
模块,注意我们已经在app.js
中移除了app.listen(3000);
语句,所以,这里我们用:
let server = app.listen(9900);
让app
实例监听在9900
端口上,并且获得返回的server
实例。
在测试代码中,我们使用:
let res = await request(server).get(‘/‘);
就可以构造一个GET请求,发送给koa的应用,然后获得响应。
可以手动检查响应对象,例如,res.body
,还可以利用supertest
提供的expect()
更方便地断言响应的HTTP代码、返回内容和HTTP头。断言HTTP头时可用使用正则表达式。例如,下面的断言:
.expect(‘Content-Type‘, /text\/html/)
可用成功匹配到Content-Type
为text/html
、text/html; charset=utf-8
等值。
当所有测试运行结束后,app
实例会自动关闭,无需清理。
利用mocha的异步测试,配合supertest,我们可以用简单的代码编写端到端的HTTP自动化测试。