본문 바로가기

javascript/이외

[오늘의 삽질] jest.fn이 undefined을 반환하는 경우

jest를 이용하여 테스트를 진행하는데, mocked 객체가 동작하지 않는 현상이 발생했다. jest.fn( )에 반환값을 명시했기 때문에 반드시 함수가 값을 반환해야 하지만, 실제로는 undefined만 반환되는 문제가 있었다. 참고로 javascript에서 아무런 값도 반환하지 않으면 undefined이므로 정확히 말하면 mock 함수가 값을 반환하지 않는 문제로 볼 수 있다.

describe('TokenInfoService', () => {
  let service: TokenInfoService;
  let repo: jest.Mocked<Repository<TokenInfo>>;
  // 작성한 mock repository 객체
  const mocked_repo = {
    save: jest.fn((tokenInfo: TokenInfo): Promise<TokenInfo> => {
      const exist_user_id = [1, 2]; // 이전에 존재하던 유저 ID
      const { id, refresh_key, user_id } = tokenInfo;
      if (exist_user_id.every((it) => it != user_id)) {
        return Promise.reject(); // 없는 유저 생성 시 발생하는 에러
        // 원래는 reference key에 의해 발생하는 에러.
      }
      return Promise.resolve({
        id: id ?? 0,
        refresh_key: refresh_key,
        updatedAt: new Date(),
        user_id,
      });
    }),
    findOneBy: jest.fn((where) => {
      const { user_id } = where;

      if (user_id === 1) {
        return Promise.resolve({
          id: 0,
          refresh_key: 'test_refresh',
          updatedAt: new Date(),
          user_id: user_id as number,
        });
      } else {
        return Promise.resolve(null);
      }
    }),
    create: jest.fn(() => new TokenInfo()),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TokenInfoService,
        // TokenInfo Repository mocking
        {
          provide: getRepositoryToken(TokenInfo),
          useValue: mocked_repo,
        },
      ],
    }).compile();

    service = module.get<TokenInfoService>(TokenInfoService);
    repo = module.get(getRepositoryToken(TokenInfo));
  });
  
  afterEach(() => {
    jest.clearAllMocks();
  });
  
  //이후 테스트 코드들
});

현재 상황은 다음과 같다.

  • mocked 객체를 beforeEach 외부에 작성한다.
  • nestjs testing module에 mocked 객체를 의존성으로 주입한다.
  • 각 함수에 대한 테스트를 진행한다.

 함수를 잘 작성했기 때문에 문제 없이 동작하여 findOneBy 함수가 객체 또는 null을 반환해야 하지만, 실제로는 어떠한 값도 반환하지 않는 현상이 있었다. 혹시 하는 마음에 beforeAll / beforeEach 내부로 mock 객체를 이동했더니, beforeAll의 경우는 기존과 동일한 결과가 나왔고, beforeEach의 경우 정상적인 결과가 나왔다.

값이 반환되기를 기대했으나, 실제로는 undefined가 나옴

  해당 결과를 바탕으로, 혹시 jest는 자동으로 mock 객체를 제거하는 것이 아닌가 싶어 구글링을 진행하다보니 다음과 같은 글을 발견했다.

 

jest.fn().mockReturnValue("hello") returns undefined when called in a beforeAll() but not in a beforeEach()

How come when I create a jest.fn() function and then call mockReturnValue("hello") on it inside of a beforeAll() I get undefined when I try to console.log the value of it in a test but no...

stackoverflow.com

위 글의 상황은 나와 일치하는 부분이 많았다. beforeAll에 정의하는 경우 undefined을 전달하고, beforeEach 내부에 정의해야만 제대로 된 값을 반환하는 것까지 일치했다.

결론은 다음과 같다. ( https://jestjs.io/docs/cli#--resetmocks )

// package.json 내부에 정의

"jest": {
    "resetMocks": false
}

 CRA, nest.js의 경우 jest를 사용할 때 기본적으로 --resetMocks 옵션이 설정되어 있는지, 모든 테스트 전에 모의 상태 및 구현 사항을 재설정한다. jest.resetAllMocks을 테스트 이전에 실행하는 것과 동일한 결과가 나오기 때문에 beforeEach가 동작하기 이전에 이미 정의되어 있는 모의 함수들을 무효화하는 것이다. 따라서 해당 옵션을 package.json 내부에 false로 정의하면 문제가 해결된다.

 모의 객체를 beforeEach 외부에 정의하면 각 테스트 기록(호출 횟수, 입력, 출력 등)이 남기 때문에, afterEach에 jest.clearAllMocks을 정의하여 각 테스트마다 테스트 기록만 제거하도록 설정하는 편이 좋다. package.json 파일에 "clearMocks": true로 설정해도 되는 것 같다.

  afterEach(() => {
    jest.clearAllMocks();
  });

언급한 옵션을 변경했더니, 테스트 코드가 정상적으로 동작한다.


참고

 jest에는 모의 함수 / 객체에 대해 reset / restore / clear 동작이 있다. 이름도 비슷해서 동작이 헷갈릴 수 있으므로, 이에 대해 간단하게 정리해 본다.

  • clearAllMocks: 구현 사항을 제외하고 모의 데이터(입력, 출력, 호출 횟수 등)를 초기화한다. fn.mock.calls(파라미터) 및 fn.mocks.results(반환값)을 초기화하는 것과 같다고 한다.
  • resetAllMocks: 모의 데이터뿐만 아니라 mockImplementation을 통한 구현 사항도 초기화한다. 초기화된 함수는 어떠한 구현도 없는 jest.fn( )이 된다.
  • restoreAllMocks: 모의 함수가 jest.spyOn에 의해 원본에서 대체된 경우, 이를 원래 함수로 복원한다. jest.spyOn에 의해 만들어진 모의 함수의 경우에만 원본으로 변경하며, 이외의 경우(jest.fn으로 생성하는 경우 등)는 현재 명령에 영향을 받지 않는다.