egg.js 中 Service 的封装 单元测试中获取不到app属性?
发布于 5 年前 作者 thomas0836 3583 次浏览 来自 问答

系统内有一堆对实体的基本的增删改查的操作,所以想抽象一个基类

'use strict';

const Service = require('egg').Service;

class LogService extends Service {

  // index======================================================================================================>
  async index(payload) {
    const { ctx } = this;
    const { helper } = ctx;
    let { page, limit, isPaging, where, sortby } = payload;

    if (where) {
      where = JSON.parse(where);
    }

    const selectObj = { where };

    if (sortby) {
      sortby = sortby.replace(', ', ',').replace(' ,', ',');
      const order = sortby.split(',');
      selectObj.order = order.map(n => n.split(' '));
    } else {
      selectObj.order = [[ 'id', 'DESC' ]];
    }

    if (!isPaging) {
      limit = helper.toInt(limit) || ctx.app.config.default_limit;
      page = helper.toInt(page) || ctx.app.config.default_page;

      selectObj.offset = (page - 1) * limit;
      selectObj.limit = limit;
    } else {
      page = ctx.app.config.default_page;
    }

    const res = await ctx.model.Log.findAndCountAll(selectObj);

    return { count: res.count, list: res.rows, limit, currentPage: page };
  }


  // show======================================================================================================>
  async show(id) {
    const { ctx } = this;
    const log = await ctx.model.Log.findById(id);
    if (!log) {
      ctx.throw(404, ctx.__('Data not found'));
    }
    return log;
  }

  // create======================================================================================================>
  async create(payload) {
    const { ctx } = this;
    return ctx.model.Log.create(payload);
  }

  // destroy======================================================================================================>
  async destroy(id) {
    const { ctx } = this;
    const { Log } = ctx.model;
    const log = await Log.findById(id);
    if (!log) {
      ctx.throw(404, ctx.__('Data not found'));
    }
    return log.destroy();
  }

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

    if (canSave) {
      const keyArr = [ 'remark' ];
      const willUpdate = ctx.helper.getKeyObj({ obj: payload, keyArr });

      await log.update(willUpdate);
      return log;
    }
  }

}

module.exports = LogService;

对应的单元测试

'use strict';

const { app } = require('egg-mock/bootstrap');
const assert = require('assert-extends');
const baseModel = 'log';

describe.only('LogService test', () => {
  describe('index(payload)', () => {
    it('should limit 2 data and order id DESC', async () => {

      // 通过 factory-girl 快速创建 数据 对象到数据库中
      const list = await app.factory.createMany(baseModel, 50);

      const returnObj = await app.mockContext().service[baseModel].index({ limit: 2 });
      assert(returnObj.count === 50);
      assert(returnObj.list.length === 2);
      assert(returnObj.list[0].creatorName === list[49].creatorName);
      assert(returnObj.list[0].remark === list[49].remark);
      assert(returnObj.list[0].id > returnObj.list[1].id);
      assert(returnObj.limit === 2);
      assert(returnObj.currentPage === 1);

    });
    it('should just one data', async () => {
      // 通过 factory-girl 快速创建 数据 对象到数据库中
      const list = await app.factory.createMany(baseModel, 50);

      const remark = list[0].remark;
      const returnObj = await app.mockContext().service[baseModel].index({ where: JSON.stringify({
        remark,
      }) });
      assert(returnObj.count === 1);
      assert(returnObj.list.length === 1);
      assert(returnObj.list[0].remark === remark);
    });

    it('should limit 20 data', async () => {
      // 通过 factory-girl 快速创建 数据 对象到数据库中
      const list = await app.factory.createMany(baseModel, 50);

      const returnObj = await app.mockContext().service[baseModel].index({});
      assert(returnObj.count === 50);
      assert(returnObj.list.length === 20);
      assert(returnObj.list[0].creatorName === list[49].creatorName);
      assert(returnObj.list[0].remark === list[49].remark);
      assert(returnObj.limit === 20);
      assert(returnObj.currentPage === 1);
    });

    it('should in page 3', async () => {
      // 通过 factory-girl 快速创建 数据 对象到数据库中
      const list = await app.factory.createMany(baseModel, 50);

      const returnObj = await app.mockContext().service[baseModel].index({ page: 3 });
      assert(returnObj.count === 50);
      assert(returnObj.list.length === 10);
      assert(returnObj.list[0].creatorName === list[9].creatorName);
      assert(returnObj.list[0].remark === list[9].remark);
      assert(returnObj.limit === 20);
      assert(returnObj.currentPage === 3);
    });

    it('should paging', async () => {
      // 通过 factory-girl 快速创建 数据 对象到数据库中
      await app.factory.createMany(baseModel, 50);

      const returnObj = await app.mockContext().service[baseModel].index({ page: 4, isPaging: true });
      assert(returnObj.count === 50);
      assert(returnObj.list.length === 50);
      assert(!returnObj.limit);
      assert(returnObj.currentPage === 1);
    });

    it('should order creatorName asc and remark desc', async () => {
      // 通过 factory-girl 快速创建 数据 对象到数据库中
      const list = await app.factory.createMany(baseModel, 50);

      const returnObj = await app.mockContext().service[baseModel].index({ sortby: '`creatorName` asc, remark desc' });
      assert(returnObj.count === 50);
      assert(returnObj.list.length === 20);
      assert(returnObj.limit === 20);
      assert(returnObj.currentPage === 1);
      assert(returnObj.list[0].creatorName === list[0].creatorName);
      assert(returnObj.list[0].remark === list[0].remark);
    });

  });

  describe('show(id)', () => {
    it('should get exists data', async () => {
      const log = await app.factory.create(baseModel);
      const returnObj = await app.mockContext().service[baseModel].show(log.id);
      assert(returnObj.remark === log.remark);
    });
    it('should not get no exists data', async () => {
      const ctx = app.mockContext();

      return assert.asyncThrows(
        async () => {
          await ctx.service[baseModel].show(10000);
        },
        /^NotFoundError: Data not found$/
      );
    });
  });

  describe('create(payload)', () => {
    it('Should can create', async () => {
      const willSave = {
        remark: 'this is 这是',
        creatorName: 'creatorName',
        userId: 1,
        creatorId: 1,
      };
      const returnObj = await app.mockContext().service[baseModel].create(willSave);
      assert(returnObj.id);
      assert(returnObj.creatorName === willSave.creatorName);
      assert(returnObj.remark === willSave.remark);
      assert(returnObj.userId === willSave.userId);
      assert(returnObj.creatorId === willSave.creatorId);
    });
    it('Validate should can‘t create', async () => {
      const willSaveValidate = {
        creatorName: 'creatorName',
      };
      return assert.asyncThrows(
        async () => {
          await app.mockContext().service[baseModel].create(willSaveValidate);
        },
        /^SequelizeValidationError:(?:.|[\r\n])*$/
      );
    });
  });

  describe('update(id, payload)', () => {
    it('should update succeed', async () => {
      const log1 = await app.factory.create(baseModel);

      const willUpdate1 = {
        remark: 'update 更新',
      };

      const willUpdate2 = {
        creatorName: 'creatorName 更新',
      };

      // 创建 ctx
      const ctx = app.mockContext();
      const logService = ctx.service[baseModel];
      let returnObj = await logService.update(log1.id, willUpdate1);
      assert(returnObj.remark === willUpdate1.remark);
      assert(returnObj.userId === log1.userId);
      assert(returnObj.creatorName === log1.creatorName);
      assert(returnObj.creatorId === log1.creatorId);

      returnObj = await logService.show(log1.id);
      assert(returnObj.remark === willUpdate1.remark);
      assert(returnObj.userId === log1.userId);
      assert(returnObj.creatorName === log1.creatorName);
      assert(returnObj.creatorId === log1.creatorId);

      returnObj = await logService.update(log1.id, willUpdate2);
      assert(returnObj.remark === willUpdate1.remark);
      assert(returnObj.userId === log1.userId);
      assert(returnObj.creatorName === log1.creatorName);
      assert(returnObj.creatorId === log1.creatorId);
      assert(returnObj.creatorName !== willUpdate2.creatorName);

      returnObj = await logService.show(log1.id);
      assert(returnObj.remark === willUpdate1.remark);
      assert(returnObj.userId === log1.userId);
      assert(returnObj.creatorName === log1.creatorName);
      assert(returnObj.creatorId === log1.creatorId);

    });
    it('should not update succeed not exist data', async () => {
      return assert.asyncThrows(
        async () => {
          await app.mockContext().service[baseModel].update(10000, {});
        },
        /^NotFoundError: Data not found$/
      );
    });
  });

  describe('destroy(id)', () => {
    it('should destroy succeed', async () => {
      const log = await app.factory.create(baseModel);

      // 创建 ctx
      const ctx = app.mockContext();
      const logService = ctx.service[baseModel];

      const returnObj = await logService.show(log.id);
      assert(returnObj);

      await logService.destroy(log.id);

      return assert.asyncThrows(
        async () => {
          await logService.show(log.id);
        },
        /^NotFoundError: Data not found$/
      );
    });
    it('should not destroy succeed', async () => {
      return assert.asyncThrows(
        async () => {
          await app.mockContext().service[baseModel].destroy(10000);
        },
        /^NotFoundError: Data not found$/
      );
    });
  });
});

屏幕快照 2018-10-10 下午4.20.49.png

然后应该有一堆的基本也是这样的实体,所以想抽象一个基类

'use strict';

const Service = require('egg').Service;

class BaseService extends Service {

  constructor({ model, updateKeyArr }) {
    super();
    this.model = model;
    this.updateKeyArr = updateKeyArr;
  }
  // index======================================================================================================>
  async index(payload) {
    const { ctx } = this;
    const { helper } = ctx;
    let { page, limit, isPaging, where, sortby } = payload;

    if (where) {
      where = JSON.parse(where);
    }

    const selectObj = { where };

    if (sortby) {
      sortby = sortby.replace(', ', ',').replace(' ,', ',');
      const order = sortby.split(',');
      selectObj.order = order.map(n => n.split(' '));
    } else {
      selectObj.order = [[ 'id', 'DESC' ]];
    }

    if (!isPaging) {
      limit = helper.toInt(limit) || ctx.app.config.default_limit;
      page = helper.toInt(page) || ctx.app.config.default_page;

      selectObj.offset = (page - 1) * limit;
      selectObj.limit = limit;
    } else {
      page = ctx.app.config.default_page;
    }

    const res = await ctx.model[this.model].findAndCountAll(selectObj);

    return { count: res.count, list: res.rows, limit, currentPage: page };
  }


  // show======================================================================================================>
  async show(id) {
    const { ctx } = this;
    const entity = await ctx.model[this.model].findById(id);
    if (!entity) {
      ctx.throw(404, ctx.__('Data not found'));
    }
    return entity;
  }

  // create======================================================================================================>
  async create(payload) {
    const { ctx } = this;
    return ctx.model[this.model].create(payload);
  }

  // destroy======================================================================================================>
  async destroy(id) {
    const { ctx } = this;
    const entity = await ctx.model[this.model].findById(id);
    if (!entity) {
      ctx.throw(404, ctx.__('Data not found'));
    }
    return entity.destroy();
  }

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

    if (canSave) {
      const willUpdate = ctx.helper.getKeyObj({ obj: payload, keyArr: this.updateKeyArr });

      await entity.update(willUpdate);
      return entity;
    }
  }

}
module.exports = BaseService;

LogService 就可以变成

'use strict';

const BaseService = require('../core/base_service');

class LogService extends BaseService {

  constructor() {
    super({
      model: 'Log',
      updateKeyArr: [ 'remark' ],
    });
  }
}

module.exports = LogService;

但是运行单元测试就全部变成这样。

14) LogService test
       destroy(id)
         should not destroy succeed:
     TypeError: Cannot read property 'app' of undefined
      at new BaseContextClass (node_modules/egg-core/lib/utils/base_context_class.js:25:20)
      at new BaseContextClass (node_modules/egg/lib/core/base_context_class.js:13:1)
      at new BaseService (app/core/base_service.js:8:5)
      at new LogService (app/service/log.js:8:5)
      at getInstance (node_modules/egg-core/lib/loader/context_loader.js:92:18)
      at ClassLoader.get (node_modules/egg-core/lib/loader/context_loader.js:27:22)
      at assert.asyncThrows (test/app/service/log.test.js:209:17)
      at /Users/thomas/Documents/projects/hc_user/node_modules/assert-extends/index.js:10:13
      at new Promise (<anonymous>)
      at Function.assert.asyncThrows (node_modules/assert-extends/index.js:7:10)
      at Context.it (test/app/service/log.test.js:207:21)
      [use `--full-trace` to display the full stack trace]

看会Egg.js 的教程 https://eggjs.org/zh-cn/basics/controller.html 也有对controller 有类似的操作。想Service应该也是可以的吧? 也有找到 https://cnodejs.org/topic/5ae7c43202591040485ba997 这样一个类似的。 为什么 这里不可以?请问大神们,哪里出错啦?

9 回复

require('egg').Service 因为这个基类的 super 入参是要求传递 app 的,而你直接 super() 了。

这类问题,建议提炼一个最小可复现仓库,来反馈问题。 譬如你这个抽象 Service 基类,其实就一个简单的示例就够了,不用贴那么多代码,会导致其他人看的效率不高的。

其实这段报错表述的挺清楚的,点击过去看看源码一下就知道了。

TypeError: Cannot read property 'app' of undefined
      at new BaseContextClass (node_modules/egg-core/lib/utils/base_context_class.js:25:20)
      at new BaseContextClass (node_modules/egg/lib/core/base_context_class.js:13:1)
      at new BaseService (app/core/base_service.js:8:5)
	  at new LogService (app/service/log.js:8:5)

谢谢 @atian25

这个app 是在哪里传进去呢? 有一些可以参考的项目吗?

module.exports = app => {
  return class BaseService extends app.Service {
    // implement
	constructor({ a, b }) {
	  super(app);
	  this.a = a;
	  this.b = b;
	}
  };
};

是这样吗? 那如果我想用下面这种方式的话,是没有办法获取吗?看文档的介绍,好似只有从this中获取的方式

const Service = require('egg').Service;

class BaseService extends Service {

  constructor({ a, b }) {
    super();
    this.a = a;
    this.b = b;
  }

Service 是 egg loader 实例化的,它只会传递 ctx 进去的,你没办法自己去实例化并传递你自己的入参的。

class BaseService extends Service {
  constructor({ ctx, model }) {
    super(ctx);
    this.model = model;
  }
}

class LogService extends BaseService {
  constructor(ctx) {
    super({
      ctx,
      model: 'Log',
    })
  }
}

或者:

class BaseService extends Service {
  init({ model }) {
    this.model = model;
  }
}

class LogService extends BaseService {
  constructor(app) {
    super(app);
    this.init({ model: 'Log' });
  }
}

@atian25

看 base_context_class 的源码,貌似是要传context呢

'use strict';

/**
 * BaseContextClass is a base class that can be extended,
 * it's instantiated in context level,
 * {@link Helper}, {@link Service} is extending it.
 */
class BaseContextClass {

  /**
   * @constructor
   * @param {Context} ctx - context instance
   * @since 1.0.0
   */
  constructor(ctx) {
    /**
     * @member {Context} BaseContextClass#ctx
     * @since 1.0.0
     */
    this.ctx = ctx;
    /**
     * @member {Application} BaseContextClass#app
     * @since 1.0.0
     */
    this.app = ctx.app;
    /**
     * @member {Config} BaseContextClass#config
     * @since 1.0.0
     */
    this.config = ctx.app.config;
    /**
     * @member {Service} BaseContextClass#service
     * @since 1.0.0
     */
    this.service = ctx.service;
  }
}

module.exports = BaseContextClass;

但是在 请求时的 Context 实例和 Application.createAnonymousContext() 获取的 context 实例应该是不一样的吧? 那这个参考项目内的 在Service 层用 ctx.throw 这些方法 会否有 问题?

嗯,说错,是 ctx

createAnonymousContext() 是不带用户信息的,因为是非请求的。你啥情况下会用到它?

@atian25 原来是这样,受教了,非常感谢

@atian25 我这边也遇到了同样的问题,我目前使用的getter解决的

class BaseService extends Service {
  get model() {
    return 'Model';
  }

  index(where) {
    return this.ctx.model[ this.model ].find(where);
  }
}

class BusinessClass extends BaseService {
  // 通过getter方法来实现传入model
  get model() {
    return 'Demo';
  }
}

请教下大神,这样用可能会造成什么其他问题么

没啥问题,

回到顶部