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

使用Vitest和依赖注入测试CastVoteCommandHandler时Mock报错求助

解决Vitest中mockResolvedValueOnce调用失败的问题

问题场景

使用Vitest测试依赖注入(DI)的CastVoteCommandHandler类,该类依赖VoteRepository。尝试mockVoteRepository时测试失败,报错如下:

TypeError: findVoteMock.mockResolvedValueOnce is not a function

原业务代码

@injectable()
export class CastVoteCommandHandler {
   public constructor(
      @inject(symbols.voteRepository)
      private readonly voteRepository: VoteRepository
   ) {}

   public async execute(payload: CastVoteData) {
      if (await this.voteRepository.hasVotedTwiceOnAnswer(payload)) {
         throw new VoteProcessingError('You can only vote once on the same answer.')
      }

      if (!(await this.voteRepository.checkIfAnswerBelongsToPool(payload))) {
         throw new VoteProcessingError('Answer does not exist in the current pool.')
      }

      const existingVote = await this.voteRepository.findVoteInPoolByVoter(payload)
      if (existingVote) {
         const { vote } = existingVote
         await this.voteRepository.updateVote({
            voteId: vote.getId(),
            answerId: payload.answerId,
         })
      } else {
         await this.voteRepository.createVote(payload)
      }

      SocketIOService.emitVote(payload.poolId)
   }
}

原测试代码

import { faker } from '@faker-js/faker'
import { ContainerSingleton } from 'container'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import { type CastVoteData } from 'modules/vote/api/schemas'

import { type VoteRepository } from 'modules/vote/infrastructure/repositories'

import { Vote } from 'modules/vote/domain/entities'

import { symbols } from 'modules/vote/symbols'

import { type CastVoteCommandHandler } from './castVoteCommandHandler'

describe('CastVoteCommandHandler', () => {
   let castVoteCommandHandler: CastVoteCommandHandler
   let voteRepository: VoteRepository
   let castVoteData: CastVoteData

   beforeEach(() => {
      const container = ContainerSingleton.getInstance()
      voteRepository = container.get(symbols.voteRepository)
      castVoteCommandHandler = container.get(symbols.castVoteCommandHandler)

      castVoteData = {
         poolId: faker.datatype.uuid(),
         voterId: faker.datatype.uuid(),
         answerId: faker.datatype.uuid(),
      }
   })

   afterEach(() => {
      vi.restoreAllMocks()
   })

   it('should successfully cast vote', async () => {
      // Arrange
      const createVoteMock = vi.mocked(voteRepository.createVote)
      const findVoteMock = vi.mocked(voteRepository.findVoteInPoolByVoter)
      const updateVoteMock = vi.mocked(voteRepository.updateVote)
      const hasVotedTwiceOnAnswerMock = vi.mocked(voteRepository.hasVotedTwiceOnAnswer)
      const checkIfAnswerBelongsToPoolMock = vi.mocked(voteRepository.checkIfAnswerBelongsToPool)

      // Mock vote data
      const mockVoteData = {
         id: faker.datatype.uuid(),
         answerId: faker.datatype.uuid(),
         voterId: faker.datatype.uuid(),
      }

      // Create mock Vote instance
      const mockVote = new Vote(mockVoteData)

      // Set up your mocks
      findVoteMock.mockResolvedValueOnce({ vote: mockVote })
      hasVotedTwiceOnAnswerMock.mockResolvedValueOnce(false)
      checkIfAnswerBelongsToPoolMock.mockResolvedValueOnce(true)

      // Act
      await castVoteCommandHandler.execute(castVoteData)

      // Assert
      expect(createVoteMock).toHaveBeenCalledWith(castVoteData)
      expect(updateVoteMock).not.toHaveBeenCalled()
   })
})

问题原因

从容器中获取的voteRepository是真实实现类的实例,而非Vitest的mock对象。vi.mocked()仅做TypeScript类型断言,不会将真实方法转换为可mock的函数,因此调用mockResolvedValueOnce时会报错。

解决方案

需要在测试前替换容器中的真实VoteRepository为Vitest mock实例,具体步骤如下:

1. 创建Mock的VoteRepository实例

在beforeEach中,用vi.fn()创建每个方法的mock,然后将这个mock实例注入容器,覆盖原有的真实实例。

2. 修改测试用例中的mock设置

直接使用mock实例的方法来设置返回值,无需再用vi.mocked()做类型断言。

修改后的测试代码

import { faker } from '@faker-js/faker'
import { ContainerSingleton } from 'container'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import { type CastVoteData } from 'modules/vote/api/schemas'

import { type VoteRepository } from 'modules/vote/infrastructure/repositories'

import { Vote } from 'modules/vote/domain/entities'

import { symbols } from 'modules/vote/symbols'

import { type CastVoteCommandHandler } from './castVoteCommandHandler'

describe('CastVoteCommandHandler', () => {
   let castVoteCommandHandler: CastVoteCommandHandler
   // 声明为Mock类型,获得mock方法提示
   let voteRepository: jest.Mocked<VoteRepository>
   let castVoteData: CastVoteData

   beforeEach(() => {
      const container = ContainerSingleton.getInstance()
      
      // 创建Mock的VoteRepository实例,每个方法都用vi.fn() mock
      voteRepository = {
         hasVotedTwiceOnAnswer: vi.fn(),
         checkIfAnswerBelongsToPool: vi.fn(),
         findVoteInPoolByVoter: vi.fn(),
         updateVote: vi.fn(),
         createVote: vi.fn()
      } as jest.Mocked<VoteRepository>

      // 替换容器中的真实实例为mock实例
      container.bind(symbols.voteRepository).toConstantValue(voteRepository)
      
      // 从容器获取handler(此时它依赖的是mock的voteRepository)
      castVoteCommandHandler = container.get(symbols.castVoteCommandHandler)

      castVoteData = {
         poolId: faker.datatype.uuid(),
         voterId: faker.datatype.uuid(),
         answerId: faker.datatype.uuid(),
      }
   })

   afterEach(() => {
      vi.clearAllMocks()
   })

   it('should successfully cast vote when no existing vote', async () => {
      // Arrange
      const mockVoteData = {
         id: faker.datatype.uuid(),
         answerId: faker.datatype.uuid(),
         voterId: faker.datatype.uuid(),
      }
      const mockVote = new Vote(mockVoteData)

      // 直接设置mock方法的返回值
      voteRepository.findVoteInPoolByVoter.mockResolvedValueOnce(null)
      voteRepository.hasVotedTwiceOnAnswer.mockResolvedValueOnce(false)
      voteRepository.checkIfAnswerBelongsToPool.mockResolvedValueOnce(true)

      // Act
      await castVoteCommandHandler.execute(castVoteData)

      // Assert
      expect(voteRepository.createVote).toHaveBeenCalledWith(castVoteData)
      expect(voteRepository.updateVote).not.toHaveBeenCalled()
   })

   it('should update vote when existing vote exists', async () => {
      // Arrange
      const mockVoteData = {
         id: faker.datatype.uuid(),
         answerId: faker.datatype.uuid(),
         voterId: faker.datatype.uuid(),
      }
      const mockVote = new Vote(mockVoteData)

      voteRepository.findVoteInPoolByVoter.mockResolvedValueOnce({ vote: mockVote })
      voteRepository.hasVotedTwiceOnAnswer.mockResolvedValueOnce(false)
      voteRepository.checkIfAnswerBelongsToPool.mockResolvedValueOnce(true)

      // Act
      await castVoteCommandHandler.execute(castVoteData)

      // Assert
      expect(voteRepository.updateVote).toHaveBeenCalledWith({
         voteId: mockVote.getId(),
         answerId: castVoteData.answerId,
      })
      expect(voteRepository.createVote).not.toHaveBeenCalled()
   })
})

额外说明

  • 使用jest.Mocked<VoteRepository>可以让TypeScript识别mock对象的方法,获得mockResolvedValueOnce等方法的类型提示。
  • 测试完成后用vi.clearAllMocks()清空mock的调用记录,避免测试间的污染。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 15:12:08