You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 23:43:22