IT/기초 지식

[Jest] mock과 jest.fn(), jest.spyOn(), jest.mock()의 간단 사용법

개발자 두더지 2023. 3. 1. 21:43
728x90

mock이란?


 mock은 한마디로 테스트에 필요한 부품값을 임의로 설정하는 것을 뜻한다. 예를 들어, 클래스 A의 단체 테스트 코드를 쓸 때, " 지금 만들고 있는 테스트에 필요한 클래스 B가 미완성이라서 테스트 코드를 쓸 수 없지 않을까?", " 지금 만들고 있는 테스트에 필요한 클래스 B의 처리 양이 너무 방대해서 변경이 힘들지 않을까?" 와 같은 벽에 부딪힐 때가 있을 것이다.

 테스트를 하는 클래스 이외의 내용을 일부러 변경하는 것도 번거롭고, 변경하는 곳의 클래스가 복잡하고 방대한 처리를 하다 보면 변경, 저장, 실행의 수만큼 수고가 생긴다. 그런 곤란한 문제가 생겼을 때 사용하는 것이 "mock"이다.

 테스트를 수행하는 클래스가 다른 클래스 메서드의 반환 값을 사용하고 있을 때 mock을 사용하여 다른 클래스 자체를 만지지 않고 해당 반환 값만 자유롭게 설정할 수 있다. 즉, 테스트에 필요한 부품을 보완할 수 있는 것을 mock이라고 한다 .ㅇ

 

 

Jest의 함수 mock (jest.fn())


 Jest의 경우 jest.fn()를 이용하여 함수를 mock화 할 수 있다. 그리고 모든 mock 함수는 .mock이라는 속성이 있다. 여기에는 mock함수가 호출됐을 때 데이터와 함수의 반환값이 기록된다.  먼저 mock 속성을 확인해보자.

  • calls  : mock 함수 호출시마다의 인수 배열
  • results : mock 함수 호출시마다의 결과 배열
  • instances : new을 사용하여 mock함수에서 인스턴스화된 오브젝트가 포함된 배열
describe("#jest.fn", () => {
  it("Check `jest.fn()` specification", () => {
    const mockFunction = jest.fn();
    expect(mockFunction("test")).toBe(undefined); // mockFunction함수의 결과가 `undefined`

    expect(mockFunction).toHaveProperty("mock"); // mockFunction함수는 mock속성을 가진다.

    expect(mockFunction.mock.calls.length).toBe(1); // mockFunction함수가 1번 호출됐다.

    expect(mockFunction.mock.calls[0]).toEqual(["test"]); // mockFunction함수가 1번 호출됐을 때, 인수는 "test"였다.

    expect(mockFunction.mock.results.length).toBe(1); // mockFunction함수의 결과가 1개이다.

    expect(mockFunction.mock.results[0].type).toBe("return"); // mockFunction함수가 1번째 호출된 결과가 정상 리턴이다.

    expect(mockFunction.mock.results[0].value).toBe(undefined); // mockFunction함수의 1번째 결과가`undefined`이다.

    expect(mockFunction.mock.instances[0]).toBe(undefined); // mockFunction함수가 new를 이용한 인스턴스화가 된 적이 없다.
  });
});

 위의 코드 결과를 보아 알 수 있듯, 단순히 jest.fn()를 이용해 mock 함수를 만든 경우, undefind가 반환값으로 설정되는 것을 알 수 있다. 

 특정 반환값을 지정하고 싶다면 mockImplementation을 이용하여 변경할 수 있다. 

 it("return `Hoge`", () => {
    const mockFunction = jest.fn().mockImplementation(() => "Hoge"); // mockFunction함수의 반환값을 Hoge로 한다.
    expect(mockFunction()).toBe("Hoge");
  });

 mockImplementation는 다음과 같이 생략이 가능하다.

 it("return `Hoge`", () => {
    const mockFunction = jest.fn(() => "Hoge");
    expect(mockFunction()).toBe("Hoge");
  });

 

 

함수가 호출될 때마다 테스트하기


 mock 함수의 호출시마다 결과를 테스트하여 확인하고 싶은 경우, mockImplementationOnce 함수를 이용한다. 기본값은 undefined가 반환된다.

 it("return true once then it returns false", () => {
    const mockFunction = jest
      .fn()
      .mockImplementationOnce(() => true)
      .mockImplementationOnce(() => false);
    expect(mockFunction()).toBe(true);
    expect(mockFunction()).toBe(false);
    expect(mockFunction()).toBe(undefined); // 기본 반환값인 `undefined`가 리턴된다.
  });
});

 위 테스트에서는 mockImplementationOnce로 첫 번째는 true, 두 번째는 false를 반환하도록 설정했다. 3번째는 아무것도 설정하지 않았기에 기본 반환값인 undefined가 반환됐다.

 

 

특정 함수를 mock화 하기


 jest.spyOn()을 사용하여 오브젝트의 특정 함수를 mock화 할 수 있다. 추가로, jest.spyOn()으로 mock화 한 경우, mockRestore을 실행하여 오리지널 함수로 돌아가는 것도 가능하다.

describe("#spyOn", () => {
  const spy = jest.spyOn(Math, "random").mockImplementation(() => 0.1); // Math.random()는 0.1을 반환, 오리지널 함수에서는 0부터 1이하를 반환한다.

  afterEach(() => {
    spy.mockRestore();
    // jest.restoreAllMocks(); // 그 외 mock화하고 있는 함수가 있다면, 이 1행으로 모든 mock화한 함수를 원래대로 되돌릴 수 있다.
  });

  it("Math.random return 1", () => {
    expect(Math.random()).toEqual(0.1);
  });

  //afterEach가 실행되어, random은 0에서 1이하를 반환한다.

  it("Math.random return under 1", () => {
    expect(Math.random()).toBeLessThan(1); // 1미만일것
    expect(Math.random() < 1).toEqual(true); // toEqual로 1미만인 것을 확인
  });
});

 

 

외부 모듈의 mock화


 외부 모듈의 axios를 사용한 아래 코드에 대한 테스트를 작성하고자한다. 테스트 코드에서는 실제로 API를 호출하지 않고 axios 부분을 모듈화한다.

import axios from "axios";

class Users {
  static all() {
    return axios.get("/users.json").then((resp) => resp.data);
  }
}

export default Users;

 외부 모듈을 mock할 때는 jest.mock을 사용한다. 첫 번째 인수에 모듈명을 설정하면, 모듈 전체를 mock화할 수 있다. 아래의 코드에서는 axios를 jest.mock("axios"); 로 작성하여 mock화한다. mock화한 모듈에 대해서는 mockResolevedValue와 mockImplementation을 이용하여 반환값을 설정하는 것이 가능하다.

  • 모듈의 mock화
    • jest.mock("axios");
  • 반환값의 설정
    • axios.get.mockResolvedValue(resp);
    • axios.get.mockImplementation(() => Promise.resolve(resp))
import axios from "axios";
import Users from "./users";

jest.mock("axios");

test("should fetch users", async () => {
  const users = [{ name: "Bob" }];
  const resp = { data: users };

  axios.get.mockResolvedValue(resp);
  //axios.get.mockImplementation(() => Promise.resolve(resp))

  await expect(Users.all()).resolves.toEqual(users);
});

 

 

mock의 리셋


  • mockFn.mockClear() : mock의 속성을 모두 리셋한다.
  • mockFn.mockReset() : mock의 속성을 모두 리셋, 설정한 mock 함수를 클리어한다.
    • 오리지널 함수가 되는 것은 아니다.
  • mockFn.mockRestore() : mock함수를 오리지널 함수로 되돌린다.
    • spyOn을 이용해 mock화한 함수만 유효하다.

아래의 함수는 모든 mock에 대해서 효과가 있다.

  • jest.clearAllMocks() : 모든 mock의 설정을 리셋한다.
  • jest.restAllMocks() : 모든 mock의 속성을 리셋하고, 설정한 mock함수를 클리어한다.
    • 오리지널 함수가 되는 것은 아니다. 
  • jest.restoreAllMocks() : 모든 mock 함수를 오리지널 함수로 되돌린다.
    • spyOn을 이용하여 mock화 함수만 유효하다.

spyOn와 jest.fn은 jest.resotreAllMocks()를 실행했을 때에, spyOn은 mock화한 Date함수가 오리지널로 돌아간다는 차이가 있다.

describe("#reset mocks with spyOn", () => {
  const mockDate = new Date("2019-01-01");
  const originalDate = new Date("2020-12-25");
  let spy = null;

  beforeEach(() => {
    //  고정값이 반환되도록 한다.
    spy = jest.spyOn(global, "Date").mockImplementation(() => mockDate);
  });

  afterEach(() => {
    spy.mockRestore();
  });

  it("jest.clearAllMocks", () => {
    // Date에 인수로 다른 일자를 대입해도, mockDate가 반환된다.
    expect(new Date("2020-02-14")).toEqual(mockDate);

    // 인수의 확인
    expect(spy.mock.calls).toEqual([["2020-02-14"]]);

    // mock함수의 반환값이 오브젝트인 경우, mockInstance는 작성되지 않는다. 
    // Jest의 issue와 관련되어 있으므로 URL을 참고→https://github.com/facebook/jest/issues/10965
    expect(spy.mock.instances).toEqual([{}]);

    // 결과의 확인
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // 모든 mock 속성을 리셋한다.
    jest.clearAllMocks();

    // mock의 속성이 모두 리셋되어 있는가를 확인한다.
    expect(spy.mock.calls).toEqual([]);
    expect(spy.mock.instances).toEqual([]);
    expect(spy.mock.results).toEqual([]);

    // mock함수는 계속해서 이용할 수 있다.
    expect(new Date("2020-12-25")).toEqual(mockDate);
  });

  it("jest.resetAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(spy.mock.calls).toEqual([["2020-12-25"]]);
    expect(spy.mock.instances).toEqual([{}]);
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    //모든 mock 속성을 리셋한다. 설정한 mock함수를 클리어한다.
    jest.resetAllMocks();

    // mock의 속성이 모두 리셋된다.
    expect(spy.mock.calls).toEqual([]);
    expect(spy.mock.instances).toEqual([]);
    expect(spy.mock.results).toEqual([]);

    // mock 함수는 리셋되어, 기본값으로 `{}`가 반환된다.
    expect(new Date("2020-12-25")).toEqual({});
  });

  it("jest.restoreAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(spy.mock.calls).toEqual([["2020-12-25"]]);
    expect(spy.mock.instances).toEqual([{}]);
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    //모든 mock함수를 오리지널 함수로 되돌린다.
    jest.restoreAllMocks();

    // mock의 속성은 리셋되지 않는다.
    expect(spy.mock.calls).toEqual([["2020-12-25"]]);
    expect(spy.mock.instances).toEqual([{}]);
    expect(spy.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // mock함수가 리셋되어, 오리지널의 Date함수가 실행된다.
    expect(new Date("2020-12-25")).toEqual(originalDate);
  });
});

describe("#reset mocks with jest.fn", () => {
  const mockDate = new Date("2019-12-21"); 
  const originalDate = new Date("2020-12-25");

  beforeEach(() => {
    Date = jest.fn(() => mockDate);
  });

  it("jest.clearAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // 리셋
    jest.clearAllMocks();

    // mock의 속성이 모두 리셋된다.
    expect(Date.mock.calls).toEqual([]);
    expect(Date.mock.instances).toEqual([]);
    expect(Date.mock.results).toEqual([]);

    // mock함수는 계속해서 사용할 수 있다.
    expect(new Date("2020-12-25")).toEqual(mockDate);
  });

  it("jest.resetAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    jest.resetAllMocks();

    // mock의 속성이 모두 리셋된다.
    expect(Date.mock.calls).toEqual([]);
    expect(Date.mock.instances).toEqual([]);
    expect(Date.mock.results).toEqual([]);

    // mock함수는 리셋되어 기본값으로는 `{}`가 반환된다.
    expect(new Date("2020-12-25")).toEqual({});
  });

  it("jest.restoreAllMocks", () => {
    expect(new Date("2020-12-25")).toEqual(mockDate);
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    jest.restoreAllMocks();

    // mock의 속성은 리셋되지 않는다.
    expect(Date.mock.calls).toEqual([["2020-12-25"]]);
    expect(Date.mock.instances).toEqual([{}]);
    expect(Date.mock.results).toEqual([{ type: "return", value: mockDate }]);

    // spyOn의 경우와 달리, jest.fn으로 함수를 mock함수로 덮어쓴 경우는 restoreAllMocks를 이용해도 오리지널 함수로 돌아가지 않는다.
    expect(new Date("2020-12-25")).not.toEqual(originalDate);
    expect(new Date("2020-12-25")).toEqual(mockDate);
  });
});

참고자료

https://qiita.com/YSasago/items/6109c5d3fbdbffa31c9f

https://qiita.com/Fudeko/items/301f8a80963dfcaafb80

728x90