egg测试的知识大全--mock、superTest、coffee

EGG框架测试

初始化

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);
  });
});
  • 框架和应用不同,应用测试当前代码,而框架是测试框架代码,所以会频繁更换 baseDir 达到测试各种应用的目的。
  • baseDir 有潜规则,我们一般会把测试的应用代码放到 test/fixtures 下,所以自动补全,也可以传入绝对路径。
  • 必须指定 framework: true,告知当前路径为框架路径,也可以传入绝对路径。
  • app 应用需要在 before 等待 ready,不然在 testcase 里无法获取部分 API
  • 框架在测试完毕后需要使用 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

mock

一个 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);
});

mock CSRF

框架的默认安全插件会自动开启 CSRF 防护, 如果完整走 CSRF 校验逻辑,那么测试代码需要先请求一次页面,通过解析 HTML 拿到 CSRF token, 然后再使用此 token 发起 POST 请求。

所以 egg-mock 对 app 增加了 app.mockCsrf() 方法来模拟取 CSRF token 的过程。 这样在使用 SuperTest 请求 app 就会自动通过 CSRF 校验。

mock service

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/);
});

mock HttpClient

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');
  });
});

SuperTest

他使用这个模块的动机是为测试HTTP提供高级抽象,同时仍允许你下载到superagent提供的低级API。

example:

您可以将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);
});

coffee

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).

fork

  • fork for spawning Node process
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();

Spawn

coffee.spawn('cat')
  .write('1')
  .write('2')
  .expect('stdout', '12')
  .expect('code', 0)
  .end();

输出正常的shell脚本。

Rule

code

coffee.fork('/path/to/file.js', [ 'args' ])
  .expect('code', 0)
  // .expect('code', 1)
  .end();

stdout / stderr

coffee.fork('/path/to/file.js', [ 'args' ])
  .expect('stdout', '12\n')
  .expect('stderr', '34\n')
  .expect('code', 0)
  .end();

custom

// 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`);

API

  • coffee.spawn
  • coffee.fork
  • coffee.Coffee

Assertion object

  • coffee.expect(type,…aegs)
coffee.spawn('echo', [ 'abcdefg' ])
  .expect('stdout', 'abcdefg')
  .expect('stdout', /^abc/)
  .expect('stdout', [ 'abcdefg', /abc/ ])
  .end();

stdout / stderr / code / error

  • coffee.notExcept(type,…args)
  • coffee.write(data)

Write data to stdin.

coffee.fork(path.join(fixtures, 'stdin.js'))
  .write('1\n')
  .write('2')
  .expect('stdout', '1\n2')
  .end();
  • coffee.writeKey(…args)

UP / DOWN / LEFT / RIGHT / ENTER / SPACE

coffee.fork(path.join(fixtures, 'stdin.js'))
  .writeKey('1', 'ENTER', '2')
  .expect('stdout', '1\n2')
  .end();

所有的参数使用key进行连接

  • coffee.waitForPrompt(bool)

如果设置了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();
  });
});
  • coffee.end([callback])

回调函数将会在执行完断言之后返回,如果抛出异常第一个参数是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));
  • coffee.debug(level)

Write data to process.stdout and process.stderr for debug

level can be

  • 0 (default): pipe stdout + stderr
  • 1: pipe stdout
  • 2: pipe stderr
  • false: disable
  • coffee.coverage()
  • coffee.beforeScript(scriptFile)
  • coffee.Rule

Loader

  • 框架引入插件能够抽离出一个自己的框架j基于Egg扩展出一个框架,,所以在项目中启用插件的方法变得简单。
  • 插件加载优先级

插件→框架→应用

插件之间顺序加载,被依赖方先加载

框架按继承顺序加载,越底层越先加载

你可能感兴趣的:(node)