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

NestJS+TypeORM单元测试报save方法未定义错误排查

问题背景

我刚接触单元测试、TypeORM与NestJS,正在给用户注册功能的仓储层编写单元测试。注册业务本身运行正常,可以顺利完成用户注册,但执行仓储单元测试时抛出错误:

Cannot read properties of undefined (reading 'save')

相关代码

user.repository.ts

import {
  ConflictException,
  InternalServerErrorException,
} from '@nestjs/common';
import { EntityRepository, Repository } from 'typeorm';
import { genSalt, hash } from 'bcryptjs';
import { omit } from 'lodash';
import { generate } from 'randomstring';
import { CreateUserDto } from 'src/dtos/user/create-user-dto';
import { User } from './user.entity';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async register(createUserDto: CreateUserDto): Promise<object> {
    const {
      firstName,
      ... //other destructured fields here
    } = createUserDto;
    const salt = await genSalt();
    const user = this.create();
    user.firstName = firstName;
    ... // created user here
    try {
      await this.save(user);
    } catch (error) {
      if (error.code === '23505') { //duplicate error
        console.log(error.message);
        // throw new ConflictException('Username or Password is Taken');
      } else {
        throw new InternalServerErrorException('THIS IS A 500 ERROR');
      }
    }
    let omitPasswordFromReturn = omit(user, ['password']);
    return omitPasswordFromReturn;
  }

  private async hashPassword(password: string, salt: string): Promise<string> {
    return hash(password, salt);
  }
}

user.repository.spec.ts

import { ConflictException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { CreateUserDto } from 'src/dtos/user/create-user-dto';
import { UserRepository } from './user.repository';

const mockCreateUserDto: CreateUserDto = {
  firstName: 'Muhammad',
  ...
};

describe('UserRepository', () => {
  let userRepository;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserRepository],
    }).compile();

    userRepository = await module.get<UserRepository>(UserRepository);
  });

  describe('user registration', () => {
    let save;

    beforeEach(() => {
      save = jest.fn();
      userRepository.create = jest.fn().mockReturnValue({ save });
    });  

    it('should be defined', () => {
      expect(userRepository.register).toBeDefined();
    })

    it('should sucessfully register user', async () => {
      save.mockResolvedValue(undefined);
      await expect(
        userRepository.register(mockCreateUserDto),
      ).resolves.not.toThrow();
    });

  });
});

我已经查阅TypeORM官方文档,也在StackOverflow检索了相关问题,但均未找到有效解决方案,目前已无排查思路。

错误原因

你mock错了save方法的挂载位置:

  • 业务代码里调用的await this.save(user),save是UserRepository继承自TypeORM Repository类的实例方法,挂载在仓储对象本身
  • 测试代码里错误地把mock的save挂在了this.create()返回的用户实体对象上,业务逻辑根本不会调用这个方法
  • 测试环境没有启动真实数据库连接,仓储实例原生的save方法缺少运行上下文,执行时就会抛出读取undefined属性的错误
修复方法

调整测试里的mock逻辑,直接mock仓储实例自身的方法即可,顺带把bcrypt的genSalt也mock掉,避免测试时真的执行密码哈希拖慢速度,修改后的测试核心代码如下:

// 顶部先mock bcrypt,避免真的执行加密逻辑
jest.mock('bcryptjs', () => ({
  genSalt: jest.fn().mockResolvedValue('test-salt'),
  hash: jest.fn().mockResolvedValue('hashed-password')
}))
// 记得补充导入InternalServerErrorException用于异常场景测试
import { ConflictException, InternalServerErrorException } from '@nestjs/common';

describe('UserRepository', () => {
  let userRepository: UserRepository;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserRepository],
    }).compile();

    // module.get是同步方法,不需要加await
    userRepository = module.get<UserRepository>(UserRepository);
    // 直接mock仓储实例的create和save方法,不要挂在create返回的对象上
    jest.spyOn(userRepository, 'create').mockReturnValue({
      firstName: 'Muhammad',
      // 补充其他mock的用户字段
      password: 'hashed-password'
    } as User);
    jest.spyOn(userRepository, 'save').mockResolvedValue(undefined);
  });

  describe('user registration', () => {
    it('should be defined', () => {
      expect(userRepository.register).toBeDefined();
    })

    it('should sucessfully register user', async () => {
      const result = await userRepository.register(mockCreateUserDto);
      // 校验save被正确调用
      expect(userRepository.save).toHaveBeenCalled();
      // 校验返回结果去掉了密码字段
      expect(result).not.toHaveProperty('password');
    });

    it('should throw 500 error when save fails with non-duplicate error', async () => {
      (userRepository.save as jest.Mock).mockRejectedValue({ code: '12345' });
      await expect(userRepository.register(mockCreateUserDto)).rejects.toThrow(InternalServerErrorException);
    })
  });
});

调整后测试即可正常执行,不会再报找不到save的错误。

内容的提问来源于stack exchange,提问作者MSadiq

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 15:15:31