NestJS+Jest下单元测试是否多余?测试必要性存疑
我目前使用NestJS作为开发框架,采用Jest执行测试。当前的问题是,编写的单元测试看起来非常简单,而且由于需要Mock数据库,无法真正测试数据库的实际行为。具体代码与场景如下:
UserService 代码
@Injectable() export default class UserService { constructor(private readonly userRepository: UserRepository) {} public async getUserById(id: number): Promise<User> { const user = await this.userRepository.byId(id); if (!user) { throw new UserNotFoundByIdError(id); } return user; } public async deleteUserById(id: number): Promise<void> { const user = await this.getUserById(id); await this.userRepository.deleteUser(user); } }
UserRepository 代码
@Injectable() export default class UserRepository { constructor( @InjectRepository(User) private readonly repo: Repository<User>, ) {} public byId(id: number): Promise<User> { return this.repo.findOne({ where: { id }}); } public async deleteUser(user: User): Promise<User> { await this.repo.delete(user.id); } }
单元测试代码(tests/unit目录)
const userRepositoryMock = createMockInstance(UserRepository); const userService = new UserService(userRepositoryMock); beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); jest.resetAllMocks(); }) describe('UserService', () => { describe('getUserById', () => { it('should return the provided user from the repository', async () => { (userRepositoryMock.byId as jest.Mock).mockImplementationOnce(() => Promise.resolve({...new User(), id: 1})) const user = await userService.getUserById(1); expect(user).toBeDefined(); expect(user.id).toEqual(1); }); it('should throw a UserNotFoundByIdError if the repository returns a false value', async () => { await expect(() => userService.getUserById(1)).rejects.toThrow(UserNotFoundByIdError) }); }); describe('deleteUserById', () => { it('should delete the user fromt he database', () => { (userRepositoryMock.byId as jest.Mock).mockImplementationOnce(() => Promise.resolve({...new User(), id: 1})); await userService.deleteUserById(1); expect(userRepositoryMock.deleteUser).toHaveBeenCalled(); }); it('should throw a UserNotFoundByIdError if the user does not exist', async () => { await expect(() => userService.deleteUserById(1)).rejects.toThrow(UserNotFoundByIdError) }); }); });
集成测试代码
let app: INestApplication; let userService: UserService; let userRepository: UserRepository; beforeAll(async () => { const testModule = await Test.createTestingModule({ imports: [ databaseProvider, UserModule, ], }).compile(); app = testModule.createNestApplication(); userService = app.get(UserService); userRepository = app.get(UserRepository); }); beforeEach(async () => { // clear database... }); afterAll(async () => { await app.close(); }); describe('UserService', () => { describe('getUserById', () => { it('should return the provided user from the repository', async () => { const createdUser = await userRepository.createUser({ name: 'foo' }); const user = await userService.getUserById(createdUser.id); expect(user).toBeDefined(); expect(user.id).toEqual(createdUser.id); }); it('should throw a UserNotFoundByIdError if the repository returns a false value', async () => { await expect(() => userService.getUserById(1)).rejects.toThrow(UserNotFoundByIdError) }); }); describe('deleteUserById', () => { it('should delete the user fromt he database', () => { const createdUser = await userRepository.createUser({ name: 'foo' }); await userService.deleteUserById(createdUser.id); await expect(() => userService.getUserById(createdUser.id)).rejects.toThrow(UserNotFoundByIdError); }); it('should throw a UserNotFoundByIdError if the user does not exist', async () => { await expect(() => userService.deleteUserById(1)).rejects.toThrow(UserNotFoundByIdError) }); }); });
我的设计仅将数据传递给服务类,上层没有其他逻辑——如果上层需要更多功能,我会创建单独的服务处理复杂逻辑,因此上层测试没有实际功能可测。现在觉得单元测试非常多余,因为它只操作Mock实体,且结构简单,编写时全是模板代码,几乎测试不到实际逻辑。我是不是忽略了什么,或者对这个问题的理解有误?
你的理解没大问题,但单元测试仍有存在价值
1. 快速反馈与迭代效率
集成测试依赖真实数据库,跑起来慢,用例越多耗时越长。单元测试能在几秒内跑完,修改代码后立刻知道逻辑是否断裂——比如你改了getUserById里的空值判断逻辑,单元测试能第一时间发现问题,不用等集成测试启动、清库、造数据这一套流程。
2. 精准定位问题
集成测试失败时,你得排查是服务层逻辑错了,还是数据库连接、Repository实现、数据初始化出了问题。单元测试只测服务层,失败了直接锁定是服务层的逻辑问题,不用在整个链路里找bug。
3. 覆盖极端边界场景
有些场景在集成测试里很难构造,比如Repository返回null、抛出数据库连接异常等,单元测试可以轻松Mock这些情况,确保服务层能正确处理。比如你可以MockuserRepository.byId抛出异常,测试服务层是否正确捕获或向上抛出(如果需要的话)。
4. 代码重构保障
如果以后要重构服务层逻辑——比如调整deleteUserById里的调用顺序,单元测试能帮你确认重构后的逻辑和原来一致,不用重新跑一遍集成测试。
可以优化单元测试的写法,减少模板代码
你现在的单元测试确实太模板化,可以做这些优化:
- 复用Mock逻辑:把常用的Mock(比如返回存在的用户、返回null)抽成辅助函数,避免重复写
mockImplementationOnce。 - 聚焦核心逻辑:不用写那种“返回用户”的冗余用例,重点测服务层的特有逻辑——也就是空值判断、异常抛出、依赖调用顺序这些。比如
deleteUserById里,确保先调用getUserById,再调用deleteUser,这是集成测试不会直接覆盖的服务层内部流程。
什么时候可以减少单元测试?
如果你的服务层完全是Repository的透传,没有任何自定义逻辑(比如连空值判断都放到Repository里),那单元测试确实意义不大。但目前你的服务层有异常抛出、调用依赖的顺序逻辑,这些还是值得用单元测试覆盖的。
内容的提问来源于stack exchange,提问作者Enthys

