单元测试的优点
- 代码质量持续有保障
- 重构正确性保障
- 增强自信心
- 自动化运行
测试框架
测试约定
测试目录结构
-
我们约定
test
目录为存放所有测试脚本的目录,测试所使用到的fixtures
和相关辅助脚本都应该放在此目录下。 -
测试文件的目录和我们需要测试的文件目录必须保持一直
-
测试脚本文件统一按 ${filename}.test.js 命名,必须以 .test.js 作为文件后缀。 一个应用的测试目录示例:
test ├── controller │ └── home.test.js └── service └── user.test.js
测试运行工具
统一使用 egg-bin 来运行测试脚本, 自动将内置的 Mocha、co-mocha、power-assert,nyc 等模块组合引入到测试脚本中, 让我们聚焦精力在编写测试代码上,而不是纠结选择那些测试周边工具和模块。
"scripts": {
"test": "egg-bin test",
"cov": "egg-bin cov"
}
mock
正常来说,如果要完整手写一个 app 创建和启动代码,还是需要写一段初始化脚本的, 并且还需要在测试跑完之后做一些清理工作,如删除临时文件,销毁 app。
常常还有模拟各种网络异常,服务访问异常等特殊情况。
所以我们单独为框架抽取了一个测试 mock 辅助模块:egg-mock, 有了它我们就可以非常快速地编写一个 app 的单元测试,并且还能快速创建一个 ctx 来测试它的属性、方法和 Service 等。
app
在测试运行之前,我们首先要创建应用的一个 app 实例, 通过它来访问需要被测试的 Controller、Middleware、Service 等应用层代码。
// test/controller/home.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/controller/home.test.js', () => {
});
ctx
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/controller/news.test.js', () => {
it('should get a ctx', () => {
const ctx=app.mockContext({
session: {
user:{name:'leo'}
}
});
assert(ctx.method === 'GET');
assert(ctx.url==='/');
assert(ctx.session.user.name == 'leo');
});
});
测试执行顺序
特别需要注意的是执行顺序,尽量保证在执行某个用例的时候执行相关代码。
describe('egg test', () => {
before(() => console.log('order 1'));
before(() => console.log('order 2'));
after(() => console.log('order 6'));
beforeEach(() => console.log('order 3'));
afterEach(() => console.log('order 5'));
it('should worker', () => console.log('order 4'));
});
异步测试
egg-bin 支持测试异步调用,它支持多种写法:
// 使用返回 Promise 的方式
it('should redirect', () => {
return app.httpRequest()
.get('/')
.expect(302);
});
// 使用 callback 的方式
it('should redirect', done => {
app.httpRequest()
.get('/')
.expect(302, done);
});
// 使用 async
it('should redirect', async () => {
await app.httpRequest()
.get('/')
.expect(302);
});
Controller 测试
app.httpRequest()
是 egg-mock
封装的 SuperTest 请求实例。
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/controller/home.test.js', () => {
it('homeController', async () => {
await app.httpRequest()
.get('/')
.expect(200)
.expect('hello');
let result = await app.httpRequest()
.get('/')
.expect(200)
.expect('hello');
assert(result.status == 200);
});
});
post
router.post('/post',controller.home.post);
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/controller/home.test.js', () => {
it('homeController',async () => {
let user={name: 'leo'};
app.mockCsrf();
await app.httpRequest()
.post('/post')
.type('form')
.send(user)
.expect(200)
.expect(user);
});
});
service
Service 相对于 Controller 来说,测试起来会更加简单, 我们只需要先创建一个 ctx,然后通过 ctx.service.${serviceName} 拿到 Service 实例, 然后调用 Service 方法即可。
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/service/news.test.js', () => {
it('newsService',async () => {
let ctx = app.mockContext();
let result=await ctx.service.news.list(1,5);
assert(result.length == 5);
});
});
Extend 测试
应用可以对 Application、Request、Response、Context 和 Helper 进行扩展。 我们可以对扩展的方法或者属性针对性的编写单元测试。
application
egg-mock 创建 app 的时候,已经将 Application 的扩展自动加载到 app 实例了, 直接使用这个 app 实例访问扩展的属性和方法即可进行测试。
app/extend/application.js
let cacheData={};
exports.cache={
get(key) {
return cacheData[key];
},
set(key,val) {
cacheData[key]=val;
}
}
test/app/extend/cache.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/app/extend/cache.test.js', () => {
it('cache',async () => {
app.cache.set('name','leo');
assert(app.cache.get('name') == 'leo');
});
});
context
Context 测试只比 Application 多了一个 app.mockContext() 步骤来模拟创建一个 Context 对象。 app/extend/context.js
exports.language=function () {
return this.get('accept-language');
}
test/app/extend/context.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/app/extend/context.test.js',() => {
let language="zh-cn";
it('cache',async () => {
const ctx=app.mockContext({
headers: {
'Accept-Language':language
}
});
//console.log('ctx.lan',ctx.lan())
assert(ctx.language() == language);
});
});
Request
通过 ctx.request 来访问 Request 扩展的属性和方法,直接即可进行测试。
module.exports={
get isChrome() {
const userAgent=this.get('User-Agent').toLowerCase();
return userAgent.includes('chrome');
}
}
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test/app/extend/request.test.js',() => {
it('cache',async () => {
const ctx=app.mockContext({
headers: {
'User-Agent':'I love Chrome'
}
});
assert(ctx.request.isChrome);
});
});
response
Response 测试与 Request 完全一致。 通过 ctx.response 来访问 Response 扩展的属性和方法,直接即可进行测试。
module.exports = {
get isSuccess() {
return this.status === 200;
},
};
describe('isSuccess()', () => {
it('should true', () => {
const ctx = app.mockContext();
ctx.status = 200;
assert(ctx.response.isSuccess === true);
});
it('should false', () => {
const ctx = app.mockContext();
ctx.status = 404;
assert(ctx.response.isSuccess === false);
});
});
### Helper
Helper 测试方式与 Service 类似,也是通过 ctx 来访问到 Helper,然后调用 Helper 方法测试。
module.exports = {
money(val) {
const lang = this.ctx.get('accept-language');
if (lang.includes('zh-cn')) {
return `¥ ${val}`;
}
return `$ ${val}`;
},
};
describe('money()', () => {
it('should RMB', () => {
const ctx = app.mockContext({
// 模拟 ctx 的 headers
headers: {
'Accept-Language': 'zh-CN,zh;q=0.5',
},
});
assert(ctx.helper.money(100) === '¥ 100');
});
it('should US Dolar', () => {
const ctx = app.mockContext();
assert(ctx.helper.money(100) === '$ 100');
});
});