集成测试因fixture与TypeORM问题本地通过但GitHub Actions报错
问题现象
本地运行所有测试套件全部正常通过,但GitHub Actions CI校验始终失败,问题仅出现在CI环境,本地无法复现。
失败用例报错为TypeError: Cannot read properties of undefined (reading 'id'),触发位置是读取查询返回结果的id属性时,说明按programId查询workout的方法返回了空数组。
相关代码
- 问题测试用例:路径
src/workout/repositories/workout.typeorm.repository.inte.ts
it('should find workout by program id', async () => { const workoutData = workoutDataBuilder({ id: HardCodedValuesEnum.workoutId, title: 'Leg Workout', }) const workout = new Workout(workoutData) const program = new Program(programFixture) program.workouts = [workout] await programRepository.save(program) const expectedWorkout = await workoutRepository.save(workout) const [receivedWorkout] = await workoutRepository.findByProgramId( HardCodedValuesEnum.programId, ) expect(receivedWorkout.id).toStrictEqual(expectedWorkout.id) })
- 仓储层实现:路径
src/workout/repositories/workout.typeorm.repository.ts
@EntityRepository(Workout) export class TypeOrmWorkoutRepository extends Repository<Workout> implements WorkoutRepository { findByProgramId(programId: string): Promise<Workout[]> { const workout = this.find({ relations: ['program'], where: { program: { id: programId, }, }, }) return Promise.resolve(workout) } }
- GitHub Actions工作流配置:
name: Tests on: push: branches: [ master ] pull_request: branches: [ master ] jobs: backend-tests: runs-on: ubuntu-latest defaults: run: working-directory: back steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} cache: 'yarn' cache-dependency-path: '**/back/yarn.lock' - run: sudo /etc/init.d/mysql start - run: mysql -e 'CREATE DATABASE corposano' -uroot -proot - run: mysql -e 'ALTER USER 'root'@'localhost' IDENTIFIED BY ""' -uroot -proot - run: yarn - run: yarn build - run: yarn jest - run: yarn test:inte - run: yarn test:e2e
CI报错日志
Run yarn test:inte yarn run v1.22.19 $ jest --testTimeout=10000 -c jest-inte.json --runInBand --forceExit FAIL src/workout/repositories/workout.typeorm.repository.inte.ts (10.821 s) ● TypeOrm Workout Repository › should find workout by program id TypeError: Cannot read properties of undefined (reading 'id') 158 | ) 159 | > 160 | expect(receivedWorkout.id).toStrictEqual(expectedWorkout.id) | ^ 161 | }) 162 | 163 | it.each([ at Object.<anonymous> (src/workout/repositories/workout.typeorm.repository.inte.ts:160:28) PASS src/athlete/repositories/typeorm-athlete.repository.inte.ts PASS src/exercise/repositories/type-orm-exercise.repository.inte.ts PASS src/program/repositories/type-orm-program.repository.inte.ts PASS src/daily-task/repositories/typeorm-workout.typeorm.repository.inte.ts PASS src/exercise/repositories/type-orm-exercise-template.repository.inte.ts Test Suites: 1 failed, 5 passed, 6 total Tests: 1 failed, 22 passed, 23 total Snapshots: 0 total Time: 20.43 s Ran all test suites.
根因定位与CI调试方案
优先排查高概率问题
- 关联保存逻辑缺陷:当前代码先给program实例挂载workouts数组再save program,之后又单独save workout。TypeORM在保存关联关系时,不同版本、不同配置下的级联保存行为不一致:如果
Program实体的workouts字段没有配置cascade: true,单独save program不会自动把workout和program的关联外键写入workout表。本地可能因为数据库有历史残留数据、TypeORM版本和CI有小差异,导致关联能查到;CI环境每次都是全新数据库,关联没存上自然查不到结果。
修复方式:要么给Program的workouts关联加上级联配置,去掉后面单独save workout的逻辑;要么保存完两个实体后,手动更新workout的programId字段再保存,不要依赖隐式的关联保存。 - 运行环境版本不一致:当前workflow配置里引用了
${{ matrix.node-version }},但没有在job下定义matrix配置,CI环境会安装Node.js的默认版本,和本地开发用的Node版本可能存在差异,部分依赖包在不同Node版本下的行为会有区别。同时要对比本地和CI安装的TypeORM版本,yarn缓存问题可能导致CI装的小版本和本地不一致,关联查询逻辑存在变动。 - 数据库初始化时序问题:CI里的MySQL启动后直接执行改密码、建库命令,没有等MySQL完全就绪,CI环境MySQL启动速度比本地慢,可能存在建库命令执行时服务还没完全启动,库表初始化不完全,后续数据写入失败。
- 默认配置差异:CI用的是Ubuntu源安装的MySQL,默认排序规则、大小写敏感策略和本地Windows/Mac环境的MySQL可能不一致,会导致关联查询匹配失败。另外当前查询没有加排序规则,不要依赖数据库默认返回顺序。
CI环境实操调试方法
- 在测试用例里加临时日志,把
programRepository.save、workoutRepository.save的返回结果,以及findByProgramId的查询结果全量打印,看CI环境保存数据时关联外键到底有没有写入、查询返回的数组长度是多少。 - 在执行test:inte步骤前加一步,执行
yarn list --pattern typeorm打印TypeORM版本,和本地版本做比对,确认依赖版本一致。 - 替换原来的MySQL启动命令,加循环等待逻辑,确认MySQL端口就绪后再执行建库、改密码操作,避免数据库初始化不完全。
- 测试用例里加前置断言,在读取id之前先断言查询结果长度为1,比如
expect(await workoutRepository.findByProgramId(HardCodedValuesEnum.programId)).toHaveLength(1),可以更明确地定位失败原因。
内容的提问来源于stack exchange,提问作者A Mehmeto
相关产品推荐
相关产品推荐

