如何在NestJS+TypeORM的E2E测试中忽略对象数组字段顺序?
解决NestJS+TypeORM E2E测试中数组顺序不一致导致的断言失败问题
以下是几个实用的解决方案,按需选择:
1. 自定义Jest匹配器,忽略数组顺序比较
直接修改断言逻辑,让比较时自动忽略数组的顺序差异。通过expect.extend扩展Jest的匹配器:
// 在测试初始化文件(如jest-e2e.config.ts关联的setup文件)中添加 expect.extend({ toEqualIgnoringArrayOrder(received, expected) { // 递归归一化对象:数组排序,对象按键排序 const normalize = (obj: any): any => { if (Array.isArray(obj)) { return obj.map(normalize).sort((a, b) => { // 用JSON序列化后的字符串排序,确保对象数组能按内容排序 return JSON.stringify(a).localeCompare(JSON.stringify(b)); }); } else if (typeof obj === 'object' && obj !== null) { const sortedObj: Record<string, any> = {}; // 对象按键名排序后处理值 Object.keys(obj).sort().forEach(key => { sortedObj[key] = normalize(obj[key]); }); return sortedObj; } return obj; }; const normalizedReceived = normalize(received); const normalizedExpected = normalize(expected); const pass = expect(normalizedReceived).toEqual(normalizedExpected); return { pass, message: pass ? () => `expected ${this.utils.printReceived(received)} not to match (ignoring array order) ${this.utils.printExpected(expected)}` : () => `expected ${this.utils.printReceived(received)} to match (ignoring array order) ${this.utils.printExpected(expected)}`, }; }, }); // TypeScript类型声明 declare global { namespace jest { interface Matchers<R> { toEqualIgnoringArrayOrder(expected: any): R; } } }
之后在测试中替换原断言:
expect(result).toEqualIgnoringArrayOrder(expectedResult);
优点:无需修改业务查询逻辑,快速适配现有测试;缺点:若数组元素无唯一标识或复杂嵌套,排序逻辑可能需调整。
2. 在实体关联中配置默认排序
从TypeORM查询根源固定数组顺序,在实体的关联装饰器(如@OneToMany、@ManyToMany)中添加默认排序规则:
import { Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { Job } from './job.entity'; import { ActivityArea } from './activity-area.entity'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; // 配置关联数组默认按id升序排列 @OneToMany(() => Job, job => job.user, { order: { id: 'ASC' } }) jobs: Job[]; @OneToMany(() => ActivityArea, area => area.user, { order: { id: 'ASC' } }) activity_areas: ActivityArea[]; }
这样所有查询该实体的关联数组时,都会自动按id升序返回,测试断言可以继续用toEqual。
优点:一劳永逸,所有查询统一顺序;缺点:需要逐个修改实体的关联定义,适合项目初期或关联字段不多的场景。
3. 封装BaseRepository统一处理默认排序
创建自定义的BaseRepository,覆盖find、findOne等方法,默认添加排序规则,所有实体Repository继承该基类:
import { Repository, EntityManager, ObjectLiteral, SelectQueryBuilder } from 'typeorm'; export class BaseRepository<T extends ObjectLiteral> extends Repository<T> { constructor(entityManager: EntityManager, entityMetadata: any) { super(entityManager, entityMetadata); } find(options?: any) { // 合并默认排序与传入的options,自定义order优先 const mergedOptions = { ...options, order: options?.order ? options.order : { id: 'ASC' }, }; return super.find(mergedOptions); } findOne(options?: any) { const mergedOptions = { ...options, order: options?.order ? options.order : { id: 'ASC' }, }; return super.findOne(mergedOptions); } // 覆盖queryBuilder,默认添加排序 createQueryBuilder(alias?: string): SelectQueryBuilder<T> { const qb = super.createQueryBuilder(alias); if (!qb.expressionMap.orderBys.length) { qb.orderBy(`${alias}.id`, 'ASC'); } return qb; } }
然后在实体中指定使用该Repository:
@Entity({ repository: BaseRepository }) export class User { // ...实体字段 }
优点:统一管理所有查询的默认排序,无需逐个修改关联或查询;缺点:若部分查询需要特殊排序,需手动传入order参数覆盖默认。
4. 测试数据初始化时固定插入顺序(临时方案)
在E2E测试的beforeEach或beforeAll中,按固定顺序插入测试数据,尽量保证数据库返回的数组顺序与预期一致。但这种方式依赖数据库引擎(比如MySQL的InnoDB在无索引时不保证插入顺序),可靠性较低,仅适合临时应急。
内容的提问来源于stack exchange,提问作者Timothée CLEAR
相关产品推荐
相关产品推荐

