egg.js 中 Service 的封装 单元测试中获取不到app属性? - CNode技术社区

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

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

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

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

没啥问题,

回到顶部

AltStyle によって変換されたページ (->オリジナル) /