eggjs 通过单元测试为API自动生成文档
发布于 1 年前 作者 a304885433 3191 次浏览 来自 分享

这是原贴地址: https://cnodejs.org/topic/586e1810df04f6ab76081dc5

最近在用eggjs写后台,也想发布api之类的文档。以前看到过通过单元测试改造生成API文档,自己还是比较认可这个理念的,于是将其拿过来,改造了一下,适用于eggjs。

首先看看现在单测的写法:

test\controller\my.test.js

'use strict';
const test = require('../../../app/common/test');

describe('test/app/controller/home.test.js', () => {
  before(test.before);

  afterEach(test.afterEach);

  it('should GET doc', () => {
    test({
      file: 'user',
      group: '用户相关API',
      title: '获取用户信息',
      method: 'get',
      url: '/user/:id',
    })
    .get('/', {
        id: { value: '123abc', type: 'String', required: true, desc: '' },
    })
    .expect('hi, egg')
    .expect(200)
  });
});

单测代码书写方式没有太大变化,只是为了方便,做了一点点包装。

app\common\test.js

'use strict';
const mm = require('egg-mock');
const assert = require('assert');

let app;
let dir;

if(!global.docTimeout) {
    after( ()=>{
        fs.writeFileSync(path.resolve(dir, './docs/index.js'), JSON.stringify(global.docs, null, 4));
        mdRender(global.docs)
        console.log('文档生成完毕')
    } )
    global.docTimeout = true
}

module.exports = (opt) => {
  class Request {
      
    constructor(opt) {
      this.req = app.httpRequest();
      this.opt = opt;
    }

    request(type, url, params) {
      return this.req[type](url).send(params).expect(200, (err, res) => {
          if(!global.docs) global.docs = []
          this.opt.params = params
          this.opt.res = res.text
          global.docs.push(this.opt)
      });
    }

    get(url, params) {
      return this.request('get', url, params);
    }

    post(url, params) {
      return this.request('post', url, params);
    }
  }

  return  new Request(opt)
};

module.exports.before = () => {
  app = mm.app();
  //记录根目录
  if(!dir){
      dir = app.config.baseDir
  }
  return app.ready();
};

module.exports.afterEach = mm.restore;

// markdown 渲染
const fs = require('fs');
const path = require('path');
function mdRender(docs){
    const mdStr = {};
    docs.forEach((obj) => {
        if (!mdStr[obj.group]) {
            mdStr[obj.group] = '';
            mdStr[obj.group] += '## ' + obj.group + '\n\n';
        }
        const fields = {};
        mdStr[obj.group] += `### ${ obj.title } \`${ obj.method }\` ${ obj.url } \n\n#### 参数\n`;
        mdStr[obj.group] += '\n参数名 | 类型 | 是否必填 | 说明\n-----|-----|-----|-----\n';
        Object.keys(obj.params).forEach(function (param) {
            const paramVal = obj.params[param];
            fields[param] = paramVal['value'];
            mdStr[obj.group] += `${ param } | ${ paramVal['type'] } | ${ paramVal['required'] ? '是' : '否' } | ${ paramVal['desc'] } \n`;
        });
        mdStr[obj.group] += '\n#### 使用示例\n\n请求参数: \n\n';
        mdStr[obj.group] += '```json\n' + JSON.stringify(fields, null, 2) + '\n```\n';
        mdStr[obj.group] += '\n返回结果:\n\n';
        if (obj.url.indexOf(':') > -1) {
            obj.url = obj.url.replace(/:\w*/g, function (word) {
            return fields[word.substr(1)];
            });
        }
        mdStr[obj.group] += '```json\n' + JSON.stringify(obj.res, null, 2) + '\n```\n';
        mdStr[obj.group] += '\n';
        fs.writeFileSync(path.resolve(dir, './docs/', obj.file + '.md'), mdStr[obj.group]);
    })
}

代码编写仓促,难免有疏漏之处。

起初另外由于不知道怎么在所有单测执行完毕后触发事件,开始尝试在process上监听exit事件,输出最后的文档。后来去eggjs提了一个issue,才知道可以直接用after。

感觉eggjs的团队,问题回答总是这样高效、快速。

6 回复

先赞一个,然后请教下「在测试里面写文档」的原因?

为什么 API 的注释要写在单测里面,为什么不直接写到 controller 或 service?如果单测里面对同个 API 测试多次呢?

@atian25 感谢回复,解释下我自己这样做的原因。

  1. service层方法加注释,还是比较清晰的,因为参数的定义直接就在方法中。service层,我也会添加注释。

  2. controller层参数,通常从query或body中获取,而内部获取参数往往是用到的时候再获取,一般注释也会添加,但比较分散,而且还不知道怎么导出。

  3. 单测需要生成的文档,是对外发布的api接口,只是针对controller层,可能文中没有表达清楚。之所以选择在单测中去做,因为修改方法后,紧接着就是修改单测代码。这样可以拿到最新的测试请求数据,以及业务调整后请求的返回值。而对于项目来说,单测是一定要写的,所以结合起来,感觉还比较好。之前接触express和koa时,感觉 通过单测来写api文档 这个思路,与我最终想要的很一致,其他的文档库,总觉得多少有点别扭。

  4. 一个接口多个单测的情况下,现在的想法是,不需要api注释的单测,可能就直接test().get(’/’, { id: 3 ).expect(200) ,后面会逐步来考虑。

got,这类感觉还是要通过「定义即文档」的方式,在 swagger 或 grpc 等地方定义,然后自动生成测试代码和文档等。

我这边正在实践 GRPC

嗯,期待下。后面持续关注。

回到顶部