const mock = require('egg-mock');
describe('test/index.test.js', () => {
let app;
before(() => {
app = mock.app({
// 转换成 test/fixtures/apps/example
baseDir: 'apps/example',
// 重要:配置 framework
framework: true,
});
return app.ready();
});
after(() => app.close());
afterEach(mock.restore);
it('should success', () => {
return app.httpRequest()
.get('/')
.expect(200);
});
});
test/fixtures
下,所以自动补全,也可以传入绝对路径。framework: true
,告知当前路径为框架路径,也可以传入绝对路径。app.close()
关闭,不然会有遗留问题,比如日志写文件未关闭导致 fd 不够。在测试多环境场景需要使用到 cache 参数,因为 mm.app
默认有缓存,当第一次加载过后再次加载会直接读取缓存,那么设置的环境也不会生效。
const mock = require('egg-mock');
describe('/test/index.test.js', () => {
let app;
afterEach(() => app.close());
it('should test on local', () => {
mock.env('local');
app = mock.app({
baseDir: 'apps/example',
framework: true,
cache: false,
});
return app.ready();
});
it('should test on prod', () => {
mock.env('prod');
app = mock.app({
baseDir: 'apps/example',
framework: true,
cache: false,
});
return app.ready();
});
});
很少场景会使用多进程测试,因为多进程无法进行 API 级别的 mock 导致测试成本很高,而进程在有覆盖率的场景启动很慢,测试会超时。但多进程测试是验证多进程模型最好的方式,还可以测试 stdout 和 stderr。
多进程测试和 mm.app
参数一致,但 app 的 API 完全不同,不过 SuperTest 依然可用。
const mock = require('egg-mock');
describe('/test/index.test.js', () => {
let app;
before(() => {
app = mock.cluster({
baseDir: 'apps/example',
framework: true,
});
return app.ready();
});
after(() => app.close());
afterEach(mock.restore);
it('should success', () => {
return app.httpRequest()
.get('/')
.expect(200);
});
});
多进程测试还可以测试 stdout/stderr,因为 mm.cluster
是基于 coffee 扩展的,可进行进程测试。
const mock = require('egg-mock');
describe('/test/index.test.js', () => {
let app;
before(() => {
app = mock.cluster({
baseDir: 'apps/example',
framework: true,
});
return app.ready();
});
after(() => app.close());
it('should get `started`', () => {
// 判断终端输出
app.expect('stdout', /started/);
});
});
Web 应用中的单元测试更加重要,在 Web 产品快速迭代的时期,每个测试用例都给应用的稳定性提供了一层保障。 API 升级,测试用例可以很好地检查代码是否向下兼容。 对于各种可能的输入,一旦测试覆盖,都能明确它的输出。 代码改动后,可以通过测试结果判断代码的改动是否影响已确定的结果。
尽量做到修改的代码能被 100% 覆盖到。
约定 test
目录为存放所有测试脚本的目录,测试所使用到的 fixtures
和相关辅助脚本都应该放在此目录下。
测试脚本文件统一按 ${filename}.test.js
命名,必须以 .test.js
作为文件后缀。
统一使用 egg-bin 来运行测试脚本, 自动将内置的 Mocha、co-mocha、power-assert,nyc 等模块组合引入到测试脚本中, 让我们聚焦精力在编写测试代码上,而不是纠结选择那些测试周边工具和模块。
egg-bin
一个 app 创建和启动代码,还是需要写一段初始化脚本的, 并且还需要在测试跑完之后做一些清理工作,如删除临时文件,销毁 app。
常常还有模拟各种网络异常,服务访问异常等特殊情况。
Mocha 使用 before/after/beforeEach/afterEach 来处理前置后置任务,基本能处理所有问题。 每个用例会按 before -> beforeEach -> it -> afterEach -> after 的顺序执行,而且可以定义多个。
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'));
});
// 使用返回 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);
});
框架的默认安全插件会自动开启 CSRF 防护, 如果完整走 CSRF 校验逻辑,那么测试代码需要先请求一次页面,通过解析 HTML 拿到 CSRF token, 然后再使用此 token 发起 POST 请求。
所以 egg-mock 对 app 增加了 app.mockCsrf()
方法来模拟取 CSRF token 的过程。 这样在使用 SuperTest 请求 app 就会自动通过 CSRF 校验。
it('should mock fengmk1 exists', () => {
app.mockService('user', 'get', () => {
return {
name: 'fengmk1',
};
});
return app.httpRequest()
.get('/user?name=fengmk1')
.expect(200)
// 返回了原本不存在的用户信息
.expect({
name: 'fengmk1',
});
});
返回service Error的测试
it('should mock service error', () => {
app.mockServiceError('user', 'get', 'mock user service error');
return app.httpRequest()
.get('/user?name=fengmk2')
// service 异常,触发 500 响应
.expect(500)
.expect(/mock user service error/);
});
describe('GET /httpclient', () => {
it('should mock httpclient response', () => {
app.mockHttpclient('https://eggjs.org', {
// 模拟的参数,可以是 buffer / string / json,
// 都会转换成 buffer
// 按照请求时的 options.dataType 来做对应的转换
data: 'mock eggjs.org response',
});
return app.httpRequest()
.get('/httpclient')
.expect('mock eggjs.org response');
});
});
他使用这个模块的动机是为测试HTTP提供高级抽象,同时仍允许你下载到superagent提供的低级API。
您可以将http.Server或函数传递给request() - 如果服务器尚未侦听连接,则它将绑定到临时端口,因此无需跟踪端口。
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/user', function(req, res) {
res.status(200).json({ name: 'john' });
});
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end(function(err, res) {
if (err) throw err;
});
mocha的例子:
describe('GET /user', function() {
it('respond with json', function(done) {
request(app)
.get('/user')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, done);
});
});
上述语句需要注意的一点是,如果你没有添加状态代码expect(即.expect(302)),superagent现在会将任何HTTP错误(除了2XX响应代码以外的任何内容)作为第一个参数发送回回调。
如果使用.end()方法.expect()失败的断言不会抛出 - 它们会将断言作为错误返回给.end()回调。为了使测试用例失败,您需要重新抛出或将错误传递给done()
describe('POST /users', function() {
it('responds with json', function(done) {
request(app)
.post('/users')
.send({name: 'john'})
.set('Accept', 'application/json')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
done();
});
});
});
describe('GET /users', function() {
it('responds with json', function() {
return request(app)
.get('/users')
.set('Accept', 'application/json')
.expect(200)
.then(response => {
assert(response.body.email, '[email protected]')
})
});
});
期望按照定义的顺序运行。在执行断言之前,此特性可用于修改响应主体或标头。
describe('POST /user', function() {
it('user.name should be an case-insensitive match for "john"', function(done) {
request(app)
.post('/user')
.send('name=john') // x-www-form-urlencoded upload
.set('Accept', 'application/json')
.expect(function(res) {
res.body.id = 'some fixed id';
res.body.name = res.body.name.toLowerCase();
})
.expect(200, {
id: 'some fixed id',
name: 'john'
}, done);
});
});
你可以用superagent做任何事情,你可以用supertest做 - 例如多部分文件上传!
request(app)
.post('/')
.field('name', 'my awesome avatar')
.attach('avatar', 'test/fixtures/avatar.jpg')
...
每次传递app或url都是不必要的,如果你正在测试同一个主机,你可以简单地用初始化app或url重新分配请求变量,每个request.VERB()调用都会创建一个新的Test。
request = request('http://localhost:5555');
request.get('/').expect(200, function(err){
console.log(err);
});
request.get('/').expect('heya', function(err){
console.log(err);
});
test command line on Node.js
npm i coffee --save-dev
mm.cluster
是基于coffee扩展的。
Coffee is useful for test command line in test frammework (like Mocha).
const coffee = require('coffee');
describe('cli', () => {
it('should fork node cli', () => {
return coffee.fork('/path/to/file.js')
.expect('stdout', '12\n')
.expect('stderr', /34/)
.expect('code', 0)
.end();
});
});
in file:
console.log(12);
console.error(34);
pass args
and opts
to child_process fork.(通过一些参数和可选参数)
coffee.fork('/path/to/file.js', [ 'args' ], { execArgv: [ '--inspect' ]})
.expect('stdout', '12\n')
.expect('stderr', '34\n')
.expect('code', 0)
.end();
and more:
coffee.fork('/path/to/file.js')
// print origin stdio
.debug()
// inject a script
.beforeScript(mockScript)
// interact with prompt
.waitForPrompt()
.write('tz\n')
// string strict equals
.expect('stdout', 'abcdefg')
// regex
.expect('stdout', /^abc/)
// multiple
.expect('stdout', [ 'abcdefg', /abc/ ])
.expect('code', 0)
.end();
coffee.spawn('cat')
.write('1')
.write('2')
.expect('stdout', '12')
.expect('code', 0)
.end();
输出正常的shell脚本。
coffee.fork('/path/to/file.js', [ 'args' ])
.expect('code', 0)
// .expect('code', 1)
.end();
coffee.fork('/path/to/file.js', [ 'args' ])
.expect('stdout', '12\n')
.expect('stderr', '34\n')
.expect('code', 0)
.end();
// test/fixtures/extendable
const { Coffee, Rule } = require('coffee');
class FileRule extends Rule {
constructor(opts) {
super(opts);
// `args` is which pass to `expect(type, ...args)`, `expected` is the first args.
const { args, expected } = opts;
}
assert(actual, expected, message) {
// do sth
return super.assert(fs.existsSync(expected), true, `should exists file ${expected}`);
}
}
class MyCoffee extends Coffee {
constructor(...args) {
super(...args);
this.setRule('file', FileRule);
}
static fork(modulePath, args, opt) {
return new MyCoffee({
method: 'fork',
cmd: modulePath,
args,
opt,
});
}
}
// test/custom.test.js
const coffee = require('MyCoffee');
coffee.fork('/path/to/file.js', [ 'args' ])
.expect('file', `${root}/README.md`);
.notExpect('file', `${root}/not-exist`);
Assertion object
coffee.spawn('echo', [ 'abcdefg' ])
.expect('stdout', 'abcdefg')
.expect('stdout', /^abc/)
.expect('stdout', [ 'abcdefg', /abc/ ])
.end();
stdout
/ stderr
/ code
/ error
Write data to stdin.
coffee.fork(path.join(fixtures, 'stdin.js'))
.write('1\n')
.write('2')
.expect('stdout', '1\n2')
.end();
UP
/ DOWN
/ LEFT
/ RIGHT
/ ENTER
/ SPACE
coffee.fork(path.join(fixtures, 'stdin.js'))
.writeKey('1', 'ENTER', '2')
.expect('stdout', '1\n2')
.end();
所有的参数使用key进行连接
如果设置了false,coffee将会立刻输出,否则将会等待提示信息
coffee.fork('/path/to/cli', [ 'abcdefg' ])
.waitForPrompt()
.write('tz\n')
// choose the second item
.writeKey('DOWN', 'DOWN', 'ENTER');
.end(done);
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function ask(q, callback) {
process.send({ type: 'prompt' });
rl.question(q, callback);
}
ask('What\'s your name? ', answer => {
console.log(`hi, ${answer}`);
ask('How many coffee do you want? ', answer => {
console.log(`here is your ${answer} coffee`);
rl.close();
});
});
回调函数将会在执行完断言之后返回,如果抛出异常第一个参数是Error
coffee.fork('path/to/cli')
.expect('stdout', 'abcdefg')
.end(done);
// recommended to left undefind and use promise style.
const { stdout, stderr, code } = await coffee.fork('path/to/cli').end();
assert(stdout.includes(abcdefg));
Write data to process.stdout and process.stderr for debug
level
can be
插件→框架→应用
插件之间顺序加载,被依赖方先加载
框架按继承顺序加载,越底层越先加载