
요즘 읽는 책도 그렇고 모두 테스트코드에 진심인 것 같아서 많이 많이 공부하는 중입니다. 유연한 테스트 코드는 리팩터링에도 유용하고, 문서화 하기에도 유용합니다.
실제로 실무에서도 많은 도움이되고있다고 생각합니다. 예시를 하나 들자면 최근 회사 코드의 테스트 코드를 올리는 작업을 했고, 그 작업을 하던 중 하나의 Dropdown 컴포넌트가 예상했던 테스트를 계속 실패하는것을 보았습니다.
왜 안돼.. 생각도 하고 거의 3시간을 그 코드랑 싸웠는데 원인은 useEffect에 있는 의존성 배열에 들어가야하는 dropdown list가 없었던 것입니다.. 의존성 배열엔 없는데, useEffect안에서는 사용하고 조작하고 있었던 겁니다..
제가 작성한 코드가 아니였어서 여태 문제가 있었다는걸 인지하지 못했지만.. 정말 큰 충격이였습니다. 그 일이 있고부터는 테스트 코드에 좀 더 진심이 되었달까요.. 순서대로 동작하는것을 테스트하는 유닛테스트에서는 정말 정말 유용하고 미리 작성했다면 이런 문제가 없었겠다 싶었습니다.
서론은 여기까지만 하고 오늘은 테스트코드 step3입니다! jest.fn()과 jest.mock()을 활용해 테스트 코드를 유연하게 작성하는 방법에대해 알아보도록 하겠습니다.
Mock Functions?
jest.fn과 jest.mock은 둘다 mock Function이라고 불립니다. spy 코드는 다음에 다루도록 하고 오늘은 fn, mock을 다루는 방법을 중점적으로 작성하려고 합니다.
사실 이전에 jest.fn을 사용한 예제가 있습니다. React-Query 로직 테스트 코드 에서 react-query에 들어가는 function 함수를 jest.fn 을 통해서 모킹하고 return data를 출력할때 사용했었습니다.
오늘은 일반적인 함수에서 사용할 예정입니다. JSX 컴포넌트와 resultFunction, exportResultFunction을 하나 만들어보겠습니다.
import fn from "@/utils/exportResultFunction"; import { minusFunction, plusFunction } from "@/utils/resultFunction"; import { useState } from "react"; const Jest = () => { const [number, setNumber] = useState(10); return ( <div> <button onClick={() => window.alert("alert")}>alert</button> <span>{number}</span> <button onClick={() => setNumber((i) => plusFunction(i))}>plus</button> <button onClick={() => setNumber((i) => minusFunction(i))}>minus</button> <span>{number}</span> <button onClick={() => setNumber((i) => fn.plusFunction(i))}>exportPlus</button> <button onClick={() => setNumber((i) => fn.minusFunction(i))}>exportMinus</button> </div> ); }; export default Jest;
// resultFunction.ts export const plusFunction = (n: number) => { return n + 1; }; export const minusFunction = (n: number) => { return n - 1; }; //exportResultFunction.ts const plusFunction = (n: number) => { return n + 1; }; const minusFunction = (n: number) => { return n - 1; }; export default { plusFunction, minusFunction, };
Jest.mock()
jest.mock()
은 전체 모듈을 모킹하기 위해 사용됩니다. 이 함수는 지정된 모듈의 모든 내보내기(exports)를 자동으로 모킹하고, 필요한 경우 모듈의 특정 부분만을 재정의할 수 있게 해줍니다. jest.mock()
은 종종 외부 모듈이나 자체 모듈의 의존성을 격리시키고, 테스트 중에 그 동작을 제어하기 위해 사용됩니다.jest.mock()
은 모듈의 모든 함수가 기본적으로 Jest의 모의 함수로 대체된다는 점에서 jest.fn()
과 차이가 있습니다. 따라서, 개별 함수를 모킹하는 대신에 전체 모듈을 모킹하고자 할 때 사용합니다.간단한 예시)
// utils.js 모듈 export const fetchData = () => { // 실제 구현 }; // 테스트 파일 jest.mock('./utils'); // utils 모듈 전체를 모킹합니다. // 이제 utils.fetchData는 자동으로 모의 함수로 대체됩니다.
가끔가다가 외부 라이브러리를 불러와서 사용하는 코드의 테스트 코드를 작성할때 에러가 나는 경우가 있습니다. 실무 기준으로는 swiper와, i18next가 있었는데요, 이런 라이브러리를 사용할때도 종종 모킹하곤 합니다.
위에서 작성한 JSX기준으로 jest.mock을 활용한 테스트 코드 예제를 작성해보겠습니다.
import { fireEvent, render } from "@testing-library/react"; import Jest from "./page"; import { minusFunction, plusFunction } from "utils/resultFunction"; jest.mock("utils/resultFunction"); describe("<Page /> 테스트 코드", () => { it("plus 테스트", () => { const { getByRole } = render(<Jest />); const plusBtn = getByRole("button", { name: "plus" }); fireEvent.click(plusBtn); expect(plusFunction).toBeCalledWith(10); }); it("minus 테스트", () => { const { getByRole } = render(<Jest />); const minusBtn = getByRole("button", { name: "minus" }); fireEvent.click(minusBtn); expect(minusFunction).toBeCalledWith(10); }); });
utils/resultFunction.ts
의 전체 모듈을 모킹하고, 그 중 사용하는 plusFunction과 minusFunction 을 가져옵니다. 따로 콜백함수가 없으면 모든 내용을 알아서 모킹합니다. 명시적으로 하고싶으면jest.mock("utils/resultFunction", () => ({ plusFunction: jest.fn(), minusFunction: jest.fn(), }));
처럼 명시적으로 작성해줄 수 있습니다.
Jest.fn()
jest.fn()
은 개별 함수를 모킹하기 위해 사용됩니다. 이 메소드는 새로운 모의 함수(mock function)를 생성하며, 이 함수는 호출될 때마다 그 호출 정보를 기록합니다. jest.fn()
을 사용하면 함수가 어떻게 호출되었는지, 몇 번 호출되었는지, 어떤 인자들로 호출되었는지 등을 검사할 수 있습니다. 또한, 이 함수의 반환 값을 설정하거나, 특정 상황에서 예외를 발생시키도록 설정할 수도 있습니다.간단한 예시)
const mockFunction = jest.fn(); mockFunction.mockReturnValue("mock value"); console.log(mockFunction()); // "mock value"
위에 작성한JSX에 맞는 테스트 코드를 jest.fn으로 작성해보도록 하겠습니다.
import { fireEvent, render } from "@testing-library/react"; import Jest from "./page"; import fn from "utils/exportResultFunction"; fn.plusFunction = jest.fn(); fn.minusFunction = jest.fn(); const plusFn = fn.plusFunction; const minusFn = fn.minusFunction; describe("<Page /> 테스트 코드", () => { it("plus 테스트", () => { const { getByRole } = render(<Jest />); const plusBtn = getByRole("button", { name: "exportPlus" }); fireEvent.click(plusBtn); expect(plusFn).toBeCalledWith(10); }); it("minus 테스트", () => { const { getByRole } = render(<Jest />); const minusBtn = getByRole("button", { name: "exportMinus" }); fireEvent.click(minusBtn); expect(minusFn).toBeCalledWith(10); }); });
이렇게 export default되어있는 exportResultFunction 모듈을 불러와서 각각 jest.fn() 로 개별 모킹할 수 있습니다. 만약 export default가 아닌 상태라면 readOnly상태라서 jest.fn()로 모킹할 수 없습니다.
이렇게 jest.mock는 모듈 단위로, jest.fn은 개별 단위로 모킹할 수 있습니다. 이렇게 모킹한 코드는 특정 함수나, 라이브러리, api fetch등 유용하게 사용할 수 있고, 견고한 모킹은 리팩터링시 많은 도움을 줄 수 있습니다.
ex.. 위에서 설명한 useEffect 같은 경우…
오늘도 두서없는 말이였지만 읽어주셔서 감사합니다.! 위 코드의 예제는 Github 에 올려두었습니다.