两个月以前,在 《当然我在扯淡》 一文中就提到开发之外的两个重要的点:持续集成 与 测试,其中测试作为持续集成的一个重要环节,可以让我们提前知道错误代码减少自测的时间成本。在那之后,我也研究过一段时间 js 测试框架,但不知道测啥,也不知道具体咋用。最近写 node 爬虫突然顿悟,本文记录过程,不要被市面上那么多的测试框架唬住,只要知道两个名词就能玩转 js 测试。
目录
1. 上古时代测试
2. 第一个要知道的名词 —— 断言库
3. 第二个要知道的名词 —— 测试框架
4. 测试代码示例
一、上古时代的测试
先确保您的机器安装了 Node 环境,并保证您知晓最基础的 fs api。
- 创建 index.js 文件,写入如下内容。
const fs = require('fs');
function writeFile (file, content) {
return new Promise(function (resolve, reject) {
fs.writeFile(file, content, function (err) {
if (err) reject({ code: '9999', message: JSON.stringify(err) });
resolve({ code: '0000', message: '操作成功。' });
});
});
}
module.exports = writeFile;
上述代码定义了一个方法并导出,如果方法运行成功返回 { code: '0000', message: '操作成功。' }
,运行失败则返回 { code: '9999', message: JSON.stringify(err) }
。
- 创建 test.js 文件,写入如下内容。
const writeFile = require('./index');
async function start () {
const result = await writeFile('target.txt', 'Hello World!');
if (result.code !== '0000') {
console.log('\033[31m 写文件失败。 \033[39m');
} else {
console.log('\033[36m 写文件成功。 \033[39m');
}
}
start();
在 test.js 文件中引用 index.js 中提供的方法,并根据返回对象 code 值判断 writeFile 方法是否运行成功。
- 运行 test.js 文件。
D:\>node test.js
写文件成功。
上面简单的三个步骤就是最原始的 js 测试:写一个测试方法 start 去调用要测试的方法 writeFile,通过判断返回值,确定 writeFile 方法是否运行正常。
二、断言库
assert 断言库
- 创建 assert.js 文件,写入如下内容。
const assert = require('assert');
const writeFile = require('./index');
async function start () {
const result = await writeFile('target.txt', 'Hello World!');
assert.ok(result.code === '0000');
}
start();
眼尖的同学可以看到这里引入了一个新的包 assert
,这是 node 原生 api,很久以前我只见过它,它对我来说充满了神秘感。
这里调用了 assert.ok()
方法,如果 assert.ok( flag )
的 flag 值为 false,结果就会抛出一个错误,说明 writeFile 方法 运行错误 了,如果什么都没有打印,说明 运行正常,这一点有点像 linux 哲学:没有打印信息就是最好的结果。
- 运行 assert.js 文件。
D:\>node assert.js
可以看到没有打印信息,说明 writeFile 运行是正常的。
- 稍作修改 assert.js 文件。
const assert = require('assert');
const writeFile = require('./index');
async function start () {
const result = await writeFile('./other/target.txt', 'Hello World!');
assert.ok(result.code === '0000');
}
start();
测试再运行 assert.js 文件 > node assert.js
就会发现抛出了一个错误~This is Why?
注意 writeFile 的第一个参数变成了 './other/target.txt'
,往 other 目录下的 target.txt 写内容,但是由于 other 目录不存在,所以这种写法会报错。
- 总结
断言库就是类似 assert 这么一个东东,提供一些方法让你判断值是否为真,两个对象是否相同等等。如果不符合条件,那就抛出错误,阻断程序,告诉你测试到你的代码出问题了。
expect 断言库
这又是一个断言库,功能和 node 原生提供的 assert 断言库差不多,判断结果为 false 抛出异常,结果为 true 说明测试通过。第三方断言口,使用记得 npm install expect --save-dev
。
只不过有些人觉得 assert.ok(flag)
判断值是否为 flag 是否为 true 不直观,要写成 expect(flag).equal(true)
看着更像 人类语言。反正我是看不出什么差别,不知道你能否感到 更像人类语言 带来的便利。
市面上还有很多第三方断言库和 expect 大相径庭,都是写法上的细微差别,说到底干的事就那么个事:判断结果不满足情况就抛出错误说明测试失败,否则不打印任何信息。
三、测试框架
基本上断言库就可以满足我们的测试需求了,但实际应用还有个硬伤:上面测一个 writeFile 方法,我就需要 node assert.js
,如果要测多个方法,要多次重复运行 node 程序的过程,很是繁琐。
理想状态:创建 test 文件夹,所有测试代码都写在 test 目录下,再提供一个命令如 mocha
,自动执行 test 目录下所有测试文件,并统一给出测试结果。
mocha 测试框架就可以做到这么个需求。
- 安装
mocha 是测试库,只在开发时有用,先安装它:
> npm install mocha --save-dev
- 创建 test 目录,在 test 目录下创建 writeFile.test.js 文件,写入如下内容。
const assert = require('assert');
const writeFile = require('../index');
describe('测试 writeFile 方法', function() {
it('writeFile 写文件成功,返回 code 为 0000', async function() {
const result = await writeFile('target.txt', 'Hello World!');
assert.ok(result.code === '0000');
});
});
一个 describe 可以包含多个 it,关于二者什么意思,在你不准备写测试代码之前,请直接 copy 上面的结构或参考最下面我的测试代码自行感悟;
- 修改 package.json 的 script 属性
// ...
"scripts": {
"test": "mocha"
},
// ...
之后运行 npm run test
实际上就会运行 mocha
指令,而该指令会自动执行 test 目录下的文件。
- 运行测试代码
D:\>npm run test
> [email protected] test D:\
> mocha
测试 writeFile 方法
√ writeFile 写文件成功,返回 code 为 0000
1 passing (32ms)
mocha 框架会打印测试报告,以及测试时间,很人性化;
四、测试代码示例
下面贴上我写的 mogoose 增删改查工具库的测试代码,感兴趣的可以下载源码看。
npm install @dkvirus/mongoose-tools
。
const assert = require('assert');
const userDao = require('./userDao');
describe('测试 mongoose CRUD', function() {
describe('新增方法测试', function() {
it('create 新增数据成功,返回 code 为 0000', async function() {
const obj = { username: '新增测试用户', password: '111111' };
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
});
});
describe('删除方法测试', function() {
it('deleteMany 批量删除数据成功,返回 code 为 0000', async function() {
const obj = [{ username: '测试批量删除用户1', password: '111111' }, { username: '测试批量删除用户2', password: '111111' }];
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.deleteMany({ username: /测试批量删除用户/ });
assert.ok(result2.code === '0000');
});
it('deleteOne 删除一条数据成功,返回 code 为 0000', async function() {
const obj = { username: '测试单个删除用户', password: '111111' };
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.deleteOne({ username: /测试单个删除用户/ });
assert.ok(result2.code === '0000');
});
});
describe('修改方法测试', function() {
it('updateMany 批量更新数据成功,返回 code 为 0000', async function() {
const obj = [{ username: '批量修改用户1', password: '111111' }, { username: '批量修改用户2', password: '111111' }];
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.updateMany({ username: /批量修改用户/ }, { username: '修改后的批量修改用户' });
assert.ok(result2.code === '0000');
});
it('updateOne 修改一条数据成功,返回 code 为 0000', async function() {
const obj = { username: '修改用户', password: '111111' };
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.updateOne({ username: /修改用户/ }, { username: '修改后的修改用户' });
assert.ok(result2.code === '0000');
});
it('update 修改一条数据成功,返回 code 为 0000', async function() {
const obj = [{ username: 'update用户1', password: '111111' }, { username: 'update用户2', password: '111111' }];
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.update({ username: /update用户/ }, { username: '修改后update用户' });
assert.ok(result2.code === '0000');
});
it('update 修改多条数据成功,返回 code 为 0000', async function() {
const obj = [{ username: 'update批量用户1', password: '111111' }, { username: 'update批量用户2', password: '111111' }];
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.update({ username: /update批量用户/ }, { username: '修改后update批量用户' }, { multi: true });
assert.ok(result2.code === '0000');
});
});
describe('查询方法测试', function() {
it('find 查询数据成功,返回 code 为 0000', async function() {
const result = await userDao.find({ username: /用户/ });
assert.ok(result.code === '0000');
});
it('findOne 根据条件查询一条数据成功,返回 code 为 0000', async function() {
const result = await userDao.findOne({ username: /用户/ });
assert.ok(result.code === '0000');
});
it('findById 根据ID查询一条数据成功,返回 code 为 0000', async function() {
const obj = { username: 'findById用户', password: '111111' };
const result = await userDao.create(obj);
assert.ok(result.code === '0000');
const result2 = await userDao.findById(result.data._id);
assert.ok(result2.code === '0000');
});
it('count 查询符合条件数据个数成功,返回 code 为 0000', async function() {
const result = await userDao.count({ username: /用户/ });
assert.ok(result.code === '0000');
});
});
});
五、最后
打个广告,最近忽悠了个程序媛入行,她也在简书记录自己学习历程,希望看到这里的看官们可以去 她的简书主页 点个关注,给入门者一点小小的鼓励。