如何用Jest测试Objection.js?是否需重写page与where方法?
回答
没错,你确实需要重写page和where这些链式QueryBuilder方法,原因在于Objection ORM的QueryBuilder设计——它的每个链式调用方法(比如select、where、page)都会返回一个新的QueryBuilder实例。如果你的自定义MyQueryBuilder只覆盖了query(),调用其他方法时就会回到原生QueryBuilder,导致你的mock完全失效,无法控制返回结果。
具体解决方案
你需要让自定义QueryBuilder覆盖业务代码中用到的所有链式方法,确保整个调用链都在mock的实例上执行,并且模拟出业务代码期望的返回结构。这里是修改后的实现:
class MyQueryBuilder extends QueryBuilder { // 初始化时保存mock数据或状态 constructor(modelClass) { super(modelClass); this.mockData = usersMockDB.getUsers; // 你的mock数据源 this.filters = {}; // 记录过滤条件,方便后续断言 } // 重写query方法,返回当前实例维持链式调用 query() { return this; } // 模拟select方法,记录选中字段并返回this select(...fields) { this.selectedFields = fields; // 可选:用于后续断言验证字段选择 return this; } // 模拟orderBy方法,记录排序规则并返回this orderBy(field, direction) { this.sortField = field; this.sortDirection = direction; return this; } // 模拟where方法,记录过滤条件并返回this where(field, operator, value) { this.filters[field] = { operator, value }; return this; } // 核心:模拟page方法,返回业务代码需要的{ results, total }结构 page(pageNum, perPageNum) { // 可选:根据记录的filters模拟真实过滤逻辑 let filteredData = [...this.mockData]; if (this.filters.type) { const filterValue = this.filters.type.value.replace(/%/g, ''); filteredData = filteredData.filter(user => user.type.includes(filterValue)); } // 模拟分页逻辑 const startIdx = pageNum * perPageNum; const results = filteredData.slice(startIdx, startIdx + perPageNum); const total = filteredData.length; // 业务代码用await query接收结果,所以返回Promise解析后的结构 return Promise.resolve({ results, total }); } }
优化mock方式
不用创建新的UsersModel子类,直接mock原Users模型的QueryBuilder静态属性更简洁:
jest.spyOn(Users, 'QueryBuilder', 'get').mockReturnValue(MyQueryBuilder);
改进测试用例(用async/await替代done)
async/await比回调式的done更易读,也更容易处理错误:
describe('getUsers controller', () => { beforeAll(done => { i18next.on('initialized', () => done()); }); it('should return paginated and filtered user data', async () => { const res = await request(app) .get('/') .query({ page: 0, perPage: 5, type: 'admin' }); // 验证响应状态和结构 expect(res.status).toBe(200); expect(Object.keys(res.body).sort()).toEqual([ 'items', 'itemsOnPage', 'currentPage', 'totalPage', 'totalItems' ].sort()); // 验证分页和过滤结果 const filteredMockUsers = usersMockDB.getUsers.filter(u => u.type === 'admin'); expect(res.body.items).toHaveLength(Math.min(5, filteredMockUsers.length)); expect(res.body.totalItems).toBe(filteredMockUsers.length); // 可选:验证QueryBuilder的方法是否被正确调用 const queryInstance = Users.query(); expect(queryInstance.filters.type).toEqual({ operator: 'like', value: '%admin%' }); expect(queryInstance.sortField).toBe('id'); expect(queryInstance.sortDirection).toBe('DESC'); }); afterAll(async () => { await knex.destroy(); }); });
关键总结
- 必须覆盖所有业务代码中用到的QueryBuilder链式方法,保证调用链全程在mock实例上执行
page方法需要返回业务代码期望的{ results, total }结构,因为await query会解析这个值- 可以在mock方法中记录调用参数(比如过滤条件、排序字段),方便验证业务逻辑是否正确触发了这些查询操作
内容的提问来源于stack exchange,提问作者Vladimir Golub
相关产品推荐
相关产品推荐

