How to mock data when testing with vitest and Dependency Injection

1.9k views Asked by At

I'm trying to write tests for a class that uses dependency injection (DI) and I'm using the vitest testing framework. The class I want to test is CastVoteCommandHandler, which has a dependency on a VoteRepository. I want to mock the VoteRepository for testing purposes.

@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)
   }
}

I want to write a test for the execute method of CastVoteCommandHandler using the vitest testing framework. Here's the test code I've written:

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()
   })
})

However, the test is failing with the following error:

TypeError: findVoteMock.mockResolvedValueOnce is not a function

1

There are 1 answers

0
IvonaK On

Five beers later, after reading the documentation carefully, I came to this solution:

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

import { SocketIOService } from 'services'

import { CastVoteCommandHandler } from './castVoteCommandHandler'

describe('CastVoteCommandHandler', () => {
   let handler: CastVoteCommandHandler
   let mockVoteRepository: any
   let mockSocketIOService: any
   let payload: any

   beforeEach(() => {
      // Setup for each test
      mockVoteRepository = {
         hasVotedTwiceOnAnswer: vi.fn(),
         checkIfAnswerBelongsToPool: vi.fn(),
         findVoteInPoolByVoter: vi.fn(),
         updateVote: vi.fn(),
         createVote: vi.fn(),
      }

      mockSocketIOService = { emitVote: vi.fn() }

      // Mocking the static method emitVote of SocketIOService
      vi.spyOn(SocketIOService, 'emitVote').mockImplementation(mockSocketIOService.emitVote)

      handler = new CastVoteCommandHandler(mockVoteRepository)

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

   it('should successfully cast a vote when voting for the first time', async () => {
      mockVoteRepository.hasVotedTwiceOnAnswer.mockResolvedValue(false)
      mockVoteRepository.checkIfAnswerBelongsToPool.mockResolvedValue(true)
      mockVoteRepository.findVoteInPoolByVoter.mockResolvedValue(null)

      await handler.execute(payload)

      expect(mockVoteRepository.hasVotedTwiceOnAnswer).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.checkIfAnswerBelongsToPool).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.findVoteInPoolByVoter).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.createVote).toHaveBeenCalledWith(payload)
      expect(SocketIOService.emitVote).toHaveBeenCalledWith(payload.poolId)
   })

   it('should throw an error when a user attempts to vote twice on the same answer', async () => {
      mockVoteRepository.hasVotedTwiceOnAnswer.mockResolvedValue(true)

      await expect(handler.execute(payload)).rejects.toThrow('You can only vote once on the same answer.')

      expect(mockVoteRepository.hasVotedTwiceOnAnswer).toHaveBeenCalledWith(payload)
   })

   it('should throw an error when a user votes on an answer that does not belong to the pool', async () => {
      mockVoteRepository.hasVotedTwiceOnAnswer.mockResolvedValue(false)
      mockVoteRepository.checkIfAnswerBelongsToPool.mockResolvedValue(false)

      await expect(handler.execute(payload)).rejects.toThrow('Answer does not exist in the current pool.')

      expect(mockVoteRepository.hasVotedTwiceOnAnswer).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.checkIfAnswerBelongsToPool).toHaveBeenCalledWith(payload)
   })

   it('should successfully update an existing vote', async () => {
      const existingVote = { vote: { getId: vi.fn().mockReturnValue(faker.datatype.uuid()) } }

      mockVoteRepository.hasVotedTwiceOnAnswer.mockResolvedValue(false)
      mockVoteRepository.checkIfAnswerBelongsToPool.mockResolvedValue(true)
      mockVoteRepository.findVoteInPoolByVoter.mockResolvedValue(existingVote)

      await handler.execute(payload)

      expect(mockVoteRepository.hasVotedTwiceOnAnswer).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.checkIfAnswerBelongsToPool).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.findVoteInPoolByVoter).toHaveBeenCalledWith(payload)
      expect(mockVoteRepository.updateVote).toHaveBeenCalledWith({
         voteId: existingVote.vote.getId(),
         answerId: payload.answerId,
      })
      expect(SocketIOService.emitVote).toHaveBeenCalledWith(payload.poolId)
   })
})