mockResolvedValueOnce


BACKEND
NESTJS

mockResolvedValueOnce

mockResolvedValueOnce(value)Jest에서 비동기 함수(Mock 함수)가 특정 호출에서 한 번만 지정된 값을 반환하도록 설정하는 기능입니다.

따라서 한 번만 특정 값을 반환한 후, 다음 호출에서는 다른 값 반환 가능합니다.

사용 사례

mockResolvedValue 대신 mockResolvedValueOnce을 사용하는 사례를 알아봅시다.


다음은 간단한 create API usecase 코드와 이에 대한 test code입니다.


use-case 코드

import {
  BadRequestException,
  Inject,
  Injectable,
} from "@nestjs/common";
import { CreateTechRequestDto } from "../dto/request/create-tech.request.dto";
import { UpdateTechRequestDto } from "../dto/request/update-tech.request.dto";
import { ManageTechRepositoryPort } from "../port/output/manage-tech.repository.port";

@Injectable()
export class ManageTechUseCase {
  constructor(
    @Inject("ManageTechRepositoryPort")
    private readonly manageTechRepositoryPort: ManageTechRepositoryPort,
  ) {}

...

  async create(createTechRequestDto: CreateTechRequestDto) {
    const duplicatedOne = await this.findOneByContent(
      createTechRequestDto.content,
    );

    if (duplicatedOne) {
      throw new BadRequestException("존재하는 기술입니다.");
    }

    return this.manageTechRepositoryPort.create(
      createTechRequestDto,
    );
  }

...
}

테스트 코드

  describe("create()", () => {
    it("create() 호출시, Tech를 생성하고 생성한 Tech 반환", async () => {
      // Given
      const createdTech: TechDomain = new TechDomain({
        id: 1,
        content: "nestjs",
      });

      const createDto: CreateTechRequestDto = {
        content: "nestjs",
      };

      mockRepository.findOneByContent.mockResolvedValue(
        null,
      );
      mockRepository.create.mockResolvedValue(
        createdTech,
      );

      // When
      const result =
        await manageTechUseCase.create(createDto);

      // Then
      expect(
        mockRepository.findOneByContent,
      ).toHaveBeenCalledWith(createDto.content);
      expect(mockRepository.create).toHaveBeenCalledWith(
        createDto,
      );
      expect(result).toEqual(createdTech);
    });

    it("create() 호출시, requestDTO에 이미 존재하는 tech로 요청을 하면 400 에러 발생", async () => {
      // Given
      const createDto: CreateTechRequestDto = {
        content: "nestjs",
      };

      const existingTech: TechDomain = new TechDomain({
        id: 1,
        content: "nestjs",
      });

      mockRepository.findOneByContent.mockResolvedValue(
        existingTech,
      );
      mockRepository.create.mockResolvedValue(null); // create()가 호출되지 않는 것을 검증

      // When & Then
      await expect(
        manageTechUseCase.create(createDto),
      ).rejects.toThrow(BadRequestException);

      expect(
        mockRepository.findOneByContent,
      ).toHaveBeenCalledWith(createDto.content);

      expect(mockRepository.create).not.toHaveBeenCalled(); // 중복된 경우 create()가 호출되지 않아야 함
    });
  });
 

위 테스트 코드를 실행하게 되면 실패하게 됩니다. BadRequestException과 create가 동시에 실행되버리죠.


어떻게 두 코드가 동시에 실행되는 것일까요?


이유는 테스트 환경이 서로 영향을 주고 있어서입니다. 즉, 첫 번째 테스트가 실행되면서 Jest의 mock 상태가 유지되어 두 번째 테스트 실행에 영향을 미치고 있는 것입니다.


mockResolvedValue()가 Jest에서 공유되는 문제

mockRepository.findOneByContent.mockResolvedValue(null);가 첫 번째 테스트에서 설정되었고, Jest의 mockResolvedValue()는 기본적으로 테스트 케이스 간 상태를 유지하므로 두 번째 테스트에서 null이 반환될 수도 있습니다.


이런식으로 서로 다른 두 테스트 코드가 충돌하여 영향을 주며 테스트를 실패합니다.


이를 해결하기 위해 mockResolvedValue() 대신 mockResolvedValueOnce()를 사용하면 됩니다.

mockResolvedValueOnce

현재 mockResolvedValue()를 사용하고 있는데, 이는 테스트 전체에서 지속적으로 유지됩니다. 대신 mockResolvedValueOnce()를 사용하면 각 테스트마다 Mock 값을 한 번만 설정하도록 변경하면 문제가 해결될 수 있습니다.


따라서 다음과 같이 테스트코드를 변경하면 해결됩니다.

describe("ManageTechUseCase", () => {
  beforeEach(() => {
    jest.clearAllMocks(); // 모든 mock 초기화
  });

  describe("create", () => {
    it("Tech를 생성하고 생성한 Tech 반환", async () => {
      // Given
      const createdTech: TechDomain = new TechDomain({
        id: 1,
        content: "nestjs",
      });

      const createDto: CreateTechRequestDto = {
        content: "nestjs",
      };

      mockRepository.findOneByContent.mockResolvedValueOnce(null);
      mockRepository.create.mockResolvedValueOnce(createdTech);

      // When
      const result = await manageTechUseCase.create(createDto);

      // Then
      expect(mockRepository.findOneByContent).toHaveBeenCalledWith(createDto.content);
      expect(mockRepository.create).toHaveBeenCalledWith(createDto);
      expect(result).toEqual(createdTech);
    });

    it("requestDTO에 이미 존재하는 tech로 요청을 하면 400 에러 발생", async () => {
      // Given
      const createDto: CreateTechRequestDto = {
        content: "nestjs",
      };

      const existingTech: TechDomain = new TechDomain({
        id: 1,
        content: "nestjs",
      });

      mockRepository.findOneByContent.mockResolvedValueOnce(existingTech);
      mockRepository.create.mockResolvedValueOnce(null); // create()가 호출되지 않는 것을 검증

      // When & Then
      await expect(manageTechUseCase.create(createDto)).rejects.toThrow(BadRequestException);

      expect(mockRepository.findOneByContent).toHaveBeenCalledWith(createDto.content);
      expect(mockRepository.create).not.toHaveBeenCalled(); // 중복된 경우 create()가 호출되지 않아야 함
    });
  });
});

추가로 beforeEach에서 jest.clearAllMocks를 해주면 이전 테스트의 mock 상태를 초기화해줄 수 있습니다.