Egg.js 内service层的 ctx.throw 单元测试代码应该怎样写?
发布于 6 年前 作者 thomas0836 4389 次浏览 来自 问答

参考了 egg-RESTfulAPI 写的RESTfulAPI 样例,里面没有单元测试代码学习。 然后自己写单元测试代码,写到 service层的 ctx.throw 就卡住了。 屏幕快照 2018-10-09 下午3.09.06.png ctx.throw 到底有没有抛出异常

用assert.doesNotThrow 来断言是可以通过测试 屏幕快照 2018-10-09 下午3.08.30.png

npm test

屏幕快照 2018-10-09 下午3.08.55.png

用assert.throws 来断言是不可以通过测试,但是我一直怀疑是第二个参数的对象写得不对 屏幕快照 2018-10-09 下午3.28.29.png

npm test

屏幕快照 2018-10-09 下午3.17.42.png

直接运行是不能到下一个断言,感觉是给抛出异常了 屏幕快照 2018-10-09 下午3.18.16.png

npm test

屏幕快照 2018-10-09 下午3.18.42.png

对doesNotThrow 测试过,是第一个参数block 内没有抛出异常是才会通过断言,那直接运行的时候抛出来的是什么? 求大大指导一下,最好提供一下你们在面对这种单元测试的时候是怎么写的。

9 回复

可以用 https://github.com/node-modules/assert-extends

// test/app/service/user.test.js
const assert = require('assert-extends');

describe('test/app/service/user.test.js', () => {
  it('should throw', async () => {
    const ctx = app.mockContext();

    // 这里要 return 或者 await 下
    return assert.asyncThrows(async () => {
	  // 测试代码
      await ctx.service.user.echo();
    }, /this is an error/);
	// 最后是需要正则式,不要用字符串,字符串的用法是错误的,在 10 里面会抛错。
  });
});

对应的 Service,其实可以直接 throw 不用 ctx.throw

// app/service/user.js
module.exports = class UserService extends Service {
  async echo() {
    await sleep(100);

    // throw by ctx
    this.ctx.throw(new Error('this is an error'));

    // could just throw it
    // throw new Error('this is an error');
  }
};

你截图中的报错跟 assert throw 应该关系不大,而是你的 user 没挂载成功,上面看你有一句 factory 很奇怪。 最好提供下最小可复现仓库看看。

根据我上面说的自己排查下吧。

刚才提到了,应该不是测试这块的问题,你先查下为啥 user 是 undefined 吧。

另外,提供可复现仓库的意思不是截图。。。

感谢 @atian25 查了一下egg.js的 assert 是用 power-assert 的,目测是没有 assert-extends 内的 asyncThrows 方法,换成您的这个方式基本解决问题,power-assert 的 throws 不兼容 async 的方法,所以是捕获不到那个异常。

然后目前遇到一个新的情况。可以麻烦您帮忙看看吗?

async update(id, payload) {
    const { ctx } = this;
    const { User } = ctx.model;
    const user = await User.findById(id);
    let canSave = true;
    if (!user) {
      canSave = false;
      ctx.throw(404, ctx.__('Data not found'));
    }

    // 修改电话时,检查是否有重复号码
    if (payload.mobiePhone) {
      if (!await this.canUseMobiePhone({ mobiePhone: payload.mobiePhone, neId: id })) {
        canSave = false;
        ctx.throw(422, ctx.__('This phone number {0} already exists', [ payload.mobiePhone ]));
      }
    }
    if (canSave) {
      const keyArr = [ 'mobiePhone', 'email', 'nickname', 'lastAt', 'isLock', 'lastLanguage', 'headimgurl', 'genderType' ];
      const willUpdate = ctx.helper.getKeyObj({ obj: payload, keyArr });

      await user.update(willUpdate);
      return user;
    }
  }
it('should not update succeed repeat mobiePhone', async () => {
      const user1 = await app.factory.create(baseModel);
      const user2 = await app.factory.create(baseModel);
      const willUpdate = {
        mobiePhone: user2.mobiePhone,
      };
      // await app.mockContext().service.user.update(user1.id, willUpdate);
      assert.asyncThrows(
        async () => {
          await app.mockContext().service.user.update(user1.id, willUpdate);
        },
        /^UnprocessableEntityError: This phone number 86_[0-9]* already exists$/
      );
    });

我现在想对这个update方式内的422 那个异常做单元测试,在没有捕捉之前提示的异常情况是这样的

屏幕快照 2018-10-10 上午9.18.22.png

这里我也按照了您建议的正则的方式来写,但是还是不行。

屏幕快照 2018-10-10 上午9.33.11.png

更奇怪的是,同样的代码我在另外一个create 方式内也有,但是哪里就测试通过。我更是多次复制,过去还是一样的不能正确的断言到那个异常。这是为什么呢?

return assert.asyncThrows() or await assert.asyncThrows()

image.png

assert.asyncThrows() 返回的是一个 Promise,如果你没 await 或 return 的话,mocha 的测试根本来不及等待它。

@atian25 哈哈哈原来是要这样! 谢谢

@atian25 弱弱地问一下

middleware 层的类似这样的 方法该怎么写单元测试? mock 可以模拟app运行异常吗?对应的测试思路应该是怎样?

module.exports = () => {
  return async function errorHandler(ctx, next) {
    try {
      await next();
    } catch (err) {
      // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
      ctx.app.emit('error', err, ctx);

      const status = err.status || 500;
      // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
      const error = status === 500 && ctx.app.config.env === 'prod'
        ? ctx.__('Internal Server Error')
        : err.message;

      // 从 error 对象上读出各个属性,设置到响应中
      ctx.body = { error };
      if (status === 422) {
        ctx.body.detail = err.errors;
      }
      ctx.status = status;
    }
  };
};

你可以在 test 的 before 里面手动注册一个路由

describe('test/app/controller/home.test.js', () => {
  before(() => {
    app.get('/test_fail', async ctx => {
      ctx.throw(new Error('this is a test error'));
    });
  });

  it('should GET /test_fail', () => {
    return app.httpRequest()
      .get('/test_fail')
      .expect(/Error: this is a test error/)
      .expect(500);
  });
});
回到顶部