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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 12:55:07