본문 바로가기
source-code/FrontEnd

[jest] spyOn 사용 시 Cannot redefine property 에러 해결하기

by mattew4483 2024. 5. 30.
728x90
반응형

Backgounds

jest를 사용해 unit test를 작성하고 있습니다.

 

테스트 대상 모듈이 올바르게 동작하는지를 확인하기 위해

해당 모듈이 내부적으로 의존하고 있는, 다른 모듈의 반환값을 테스트 구문에서 제어할 필요가 있었습니다.

→ jest의 spyOn API를 사용해 구현할 수 있습니다.

 

https://jestjs.io/docs/jest-object#jestspyonobject-methodname

 

The Jest Object · Jest

The jest object is automatically in scope within every test file. The methods in the jest object help create mocks and let you control Jest's overall behavior. It can also be imported explicitly by via import from '@jest/globals'.

jestjs.io

// video.ts
export const video = {
  play() {
    return true;
  },
};

// video.test.ts
import { video } from './video.ts'

afterEach(() => {
  // restore the spy created with spyOn
  jest.restoreAllMocks();
});

test('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
});

 

공식 문서 속 예제처럼 사용할 수 있고,

추가적으로 mockImplementation, mockReturnValue 등의 메서드를 사용해 

해당 spy의 구현, 반환값을 테스트 구문 내에서 제어해 줄 수도 있습니다.

// video.test.ts
import { video } from './video.ts'

test('plays video', () => {
  // play mehtod의 반환값을 mocking
  jest.spyOn(video, "play").mockImplementation(() => false);
});

 

Problems

import로 특정 함수를 불러온 뒤 spyOn을 사용하고자 했습니다.

 

즉 대상 모듈은 아래와 같았고

// video.ts
export const playVideo = () => true;

 

테스트 구문에서 해당 함수의 반환값을 제어하고자 했습니다.

// video.test.ts
import * as video from './video.ts'

test('plays video', () => {
  jest.spyOn(video, "playVideo").mockReturnValue(false)
});

 

얼핏 봤을 때는 문제없이 동작할 것 같았지만... 

이 경우 Cannot redefine property: playVideo 에러가 발생하고 맙니다!

 

해당 에러는 상수에 새로운 값을 재할당하려 시도할 때 발생하는데...

playVideo 함수를 mocking하는 것 자체가 불가능한 것일까요?

그렇다면 왜 위 예제에서는 video의 play 메서드를 mocking 할 수 있었던 것일까요?

 

Caused

spyOn 메서드는 아래와 같이 사용할 수 있습니다.

jest.spyOn(object, methodName)​

Creates a mock function similar to jest.fn but also tracks calls to object[methodName]. Returns a Jest mock function.

 

즉 입력받은 object의 methodName에 해당하는 메서드를 mocking하는데...

이때 mocking 한다 = 객체의 해당 메서드를 변경한다 라고 이해할 수 있겠죠!

 

그런데 이때, 오류가 발생한 코드를 살펴보면

// video.test.ts
import * as video from './video.ts'

test('plays video', () => {
  // import한 module(video) 자체를 변경
  jest.spyOn(video, "playVideo").mockReturnValue(false)
  // 즉 video.playVideo = () => false 를 시도하는 것과 같은 행위
});

 

import as 구문을 사용해 모듈 전체를 video에 바인딩했고,

이를 통해  spyOn의 첫 번째 인자로 넘겨줄 수는 있지만 (타입 에러는 발생하지 않지만)

 

실제 테스트 코드가 동작할 때는

video.playvideo = () => false 와 같이 모듈을 직접 수정하는 행위가 발생할 것이고

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import

 

import - JavaScript | MDN

The static import declaration is used to import read-only live bindings which are exported by another module. The imported bindings are called live bindings because they are updated by the module that exported the binding, but cannot be re-assigned by the

developer.mozilla.org

이는 import 된 변수는 상수처럼 동작한다는(재할당할 수 없다는) Javascript 모듈 시스템 규칙을 어기는 게 되고 맙니다.

→ 발생한 에러 문구인 Cannot redefine property 가 이 상황을 의미하는 것이죠!

 

// video.test.ts
import { video } from './video.ts'

test('plays video', () => {
  // video 모듈이 아닌, play 속성을 수정! => Javascript 모듈 시스템에서도 문제 없음
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
});

 

 

아하! 즉 첫 예제가 정상적으로 동작했던 이유는

import 한 video란 모듈을 직접 재할당한 것이 아니라, 해당 객체의 play method를 변경한 것이기 때문에

Javscript 모듈 시스템에서도 문제가 없었던 것이죠.

 

Solution

해당 모듈 자체를 mocking

Javscript 모듈 시스템은 import한 모듈에 대한 재할당을 허용하지 않습니다.

→ 그렇다면, import한 모듈이 아닌, 해당 파일 자체를 새롭게 정의하는 것은 아무 문제가 없겠죠!

 

jest에서는 .mock API를 사용해 특정 모듈 자체를 mocking 할 수 있습니다.

// video.test.ts
import * as video from "./data";

// video.ts가 export하는 module 자체를 Mock
jest.mock("./video.ts");

test('plays video', () => {
  // 이 때 import한 video 모듈은, jest.mock 구문에 의해 재정의된 상태
  // spyOn을 통해 해당 객체의 playVideo 메서드 반환값 재정의
  jest.spyOn(video, "playVideo").mockReturnValue(false);
});

 

spyOn을 사용하기 전에, 해당 모듈 전체를 새롭게 mocking 해 위 에러를 막을 수 있습니다.

테스트 구문 전체적으로 해당 모듈의 mocking이 필요하다면 해당 구문을 jest.setup에 작성할 수도 있겠죠.

728x90
반응형