[Jest 입문하기 7] Mock Functions

Jerry·2023년 9월 2일

Jest

목록 보기
7/10
post-thumbnail

모의 함수를 사용하면 함수의 실제 구현을 지우고, 함수에 대한 호출(및 해당 호출에 전달된 매개변수)을 캡처하고, new로 인스턴스화될 때 생성자 함수의 인스턴스를 캡처하고, 테스트를 허용함으로써 코드 간 링크를 테스트할 수 있다. 이렇게 하면 테스트 시에만 반환값을 설정할 수 있다.

함수를 모의하는 방법에는 두 가지가 있다.

  • 테스트 코드에서 사용할 모의 함수를 만드는 방법
  • 모듈 종속성을 덮어 쓰기 위해 manual mock을 작성하는 방법

Using a mock function

주어진 배열의 각 항목에 대해 콜백 함수를 호출하는 forEach 함수의 구현을 테스트한다고 가정해보자.

const forEach = require("./forEach");

const mockCallback = jest.fn((x) => 42 + x);

test("forEach mock function", () => {
  forEach([0, 1], mockCallback);

  // The mock function was called twice
  expect(mockCallback.mock.calls).toHaveLength(2);
  expect(mockCallback).toHaveBeenCalledTimes(2);

  // The first argument of the first call to the function was 0
  expect(mockCallback.mock.calls[0][0]).toBe(0);
  expect(mockCallback).toHaveBeenNthCalledWith(1, 0);

  // The first argument of the second call to the function was 1
  expect(mockCallback.mock.calls[1][0]).toBe(1);
  expect(mockCallback).toHaveBeenNthCalledWith(2, 1);

  // The return value of the first call to the function was 42
  expect(mockCallback.mock.results[0].value).toBe(42);
  expect(mockCallback).toHaveNthReturnedWith(1, 42);
});

.mock property

모든 모의 함수에는 함수가 호출된 방식과 함수가 반환한 내용에 대한 데이터가 보관되는 특별한 .mock 속성이 있다. .mock 속성은 각 호출에 대해 this 값도 추적하므로 이 값도 검사할 수 있다.

const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ mockConstructor {} ]

const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ {} ]

test("", () => {});

이러한 모의 속성은 함수가 어떻게 호출되고, 인스턴스화되고, 무엇을 반환하는지 확인하는 테스트에 매우 유용하다.

test("someMockCallback", () => {
  const someMockFunction = jest.fn(function (...args) {
    this.name = args[args.length - 1];
    return "return value";
  });
  const element = new someMockFunction("first arg", "second arg", "test");

  // The function was called exactly once
  expect(someMockFunction.mock.calls).toHaveLength(1);
  expect(someMockFunction).toHaveBeenCalledTimes(1);

  // The first arg of the first call to the function was 'first arg'
  expect(someMockFunction.mock.calls[0][0]).toBe("first arg");

  // The second arg of the first call to the function was 'second arg'
  expect(someMockFunction.mock.calls[0][1]).toBe("second arg");

  // The return value of the first call to the function was 'return value'
  expect(someMockFunction.mock.results[0].value).toBe("return value");
  expect(someMockFunction).toHaveReturnedWith("return value");

  // The function was called with a certain 'this' context: the 'element' object.
  new someMockFunction("test");
  expect(someMockFunction.mock.contexts[0]).toBe(element);

  // This function was instantiated exactly twice
  expect(someMockFunction.mock.instances.length).toBe(2);

  // The object returned by the first instantiation of this function
  // had a 'name' property whose value was set to 'test'
  expect(someMockFunction.mock.instances[0].name).toBe("test");

  // The first argument of the last call to the function was 'test'
  expect(someMockFunction.mock.lastCall[0]).toBe("test");
  expect(someMockFunction).toHaveBeenLastCalledWith("test");
});

Mock Return Values

모의 함수는 테스트 중인 코드에 테스트 값을 주입하는 데에도 사용할 수 있다.

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce("x").mockReturnValue(true);

console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

test("", () => {}); // 이거 없으면 오류남

모의 함수는 함수 연속 전달(continuation-passing) 스타일을 사용하는 코드에서도 매우 효과적이다. 이 스타일로 작성된 코드는 사용되기 직전에 테스트에 직접 값을 주입하는 것을 선호하여 실제 구성 요소의 동작을 재현하는 복잡한 스텁이 필요하지 않도록 도와준다

const myMock = jest.fn();
console.log(myMock());
// > undefined

myMock.mockReturnValueOnce(10).mockReturnValueOnce("x").mockReturnValue(true);

console.log(myock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true

test("", () => {}); // 이거 없으면 오류남

const filterTestFn = jest.fn();

//
// Make the mock return 'true' for the first call,
// and 'false' for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter((num) => filterTestFn(num));
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12

//
대부분의 실제 사례에서는 의존하는 컴포넌트에 대한 모의 함수를 파악하고 구성하는 작업이 필요하지만 기술은 동일하다. 이러한 테스트를 작성하는 경우 함수 내부에서 직접 테스트되지 않은 로직을 구현하려는 유혹을 피해야 한다.


Mocking Modules

API에서 사용자를 가져오는 클래스가 있다고 가정해 보자. 클래스는 axios를 사용하여 API를 호출한 다음 모든 사용자가 포함된 데이터 속성을 반환한다.

const axios = require("axios");

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

module.exports = Users;

이제 실제로 API를 실행하지 않고(따라서 느리고 취약한 테스트를 생성하지 않고) 이 메서드를 테스트하기 위해 jest.mock(...) 함수를 사용하여 axios 모듈을 자동으로 모의할 수 있다.

모듈을 모의하고 나면 .get에 대해서 mockResolvedValue를 사용할 수 있게 되어 테스트로 검증하고 싶은 데이터를 반환하도록 할 수 있다. 실제로 우리는 axios.get('/users.json')에 가짜 응답을 반환하도록 하고 있다.

const axios = require("axios");
const Users = require("./users");

jest.mock("axios");

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

  // or you could use following depending on your use case
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then((data) => expect(data).toEqual(users));
});

Mocking Partials

모듈을 부분적으로 모의하는 것이 가능하다. 나머지는 실제 구현을 유지할 수 있다.

// foo-bar-baz.js
exports.foo = "foo";
exports.bar = function bar() {
  return "bar";
};
exports.baz = () => "baz";
// test.js
const { foo, bar, baz } = require("./foo-bar-baz");

jest.mock("./foo-bar-baz", () => {
  const originalModule = jest.requireActual("./foo-bar-baz");

  // Mock the default export and named export 'foo'
  return {
    ...originalModule,
    baz: jest.fn(() => "mocked baz"),
    foo: "mocked foo",
  };
});

test("should do a partial mock", () => {
  const bazResult = baz();
  expect(baz).toHaveReturnedWith("mocked baz");
  expect(bazResult).toBe("mocked baz");
  expect(baz).toHaveBeenCalled();

  expect(foo).toBe("mocked foo");
  expect(bar()).toBe("bar");
});

Mock Implementations

그러나 반환 값을 지정하는 기능을 넘어 모의 함수 구현으로 완전히 대체하는 것이 유용한 경우도 있다. 이는 jest.fn 또는 모의 함수의 mockImplementationOnce 메소드를 사용하여 수행할 수 있다.

const myMockFn = jest.fn((cb) => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

test("", () => {}); // 이거 없으면 오류 남

mockImplementation 메서드는 다른 모듈에서 생성된 모의 함수의 기본 구현을 정의해야 할 때 유용하다.

// foo.js
module.exports = function () {
  // some implementation
};
// test.js
const foo = require("./foo");
jest.mock("./foo");

foo.mockImplementation(() => 42);
test("foo is mock function", () => {
  expect(foo()).toBe(42);
});

함수에 대한 여러 번의 호출에서 다른 결과를 얻기 위해 복잡한 동작을 하는 모의 함수를 다시 작성해야 하는 경우 mockImplementationOnce 메서드를 사용하자

const myMockFn = jest
  .fn()
  .mockImplementationOnce(cb => cb(null, true))
  .mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

모의 함수가 mockImplementationOnce에 의해 정의된 구현을 모두 사용했을 때는, jest.fn으로 설정된 기본 구현을 실행한다(정의되어 있을 경우).

const myMockFn = jest
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

//
일반적으로 체인으로 연결된(따라서 항상 this를 반환해야 하는) 메서드가 있는 경우, 모든 모의 객체에 위치하는 .mockReturnThis() 함수 형식 안에 이 구현을 단순화하는 달콤한 API가 있다.

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};

// is the same as

const otherObj = {
  myMethod: jest.fn(function () {
    return this;
  }),
};

Mock Names

테스트 오류 출력에서 'jest.fn()' 대신 표시될 모의 함수에 대한 이름을 선택적으로 제공할 수 있다. 테스트 출력에서 오류를 보고하는 모의 함수를 빠르게 식별하려면 .mockName()을 사용하자.

const myMockFn3 = jest
  .fn()
  .mockReturnValue("default")
  .mockImplementation((scalar) => 42 + scalar)
  .mockName("add42");

Custom Matchers

마지막으로, 모의 함수가 어떻게 호출되었는지 확인하는 것을 덜 까다롭게 만들기 위해 몇 가지 사용자 정의 매처를 추가했다.

// The mock function was called at least once
expect(mockFunc).toHaveBeenCalled();

// The mock function was called at least once with the specified args
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// The last call to the mock function was called with the specified args
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// All calls and the name of the mock is written as a snapshot
expect(mockFunc).toMatchSnapshot();

이러한 매처는 .mock 속성을 검사하는 일반적인 형태를 위한 설탕이다. 원하는 경우나 좀 더 구체적인 작업이 필요한 경우 언제든지 직접 수동으로 수행할 수 있다.

// The mock function was called at least once
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);

// The mock function was called at least once with the specified args
expect(mockFunc.mock.calls).toContainEqual([arg1, arg2]);

// The last call to the mock function was called with the specified args
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
  arg1,
  arg2,
]);

// The first arg of the last call to the mock function was `42`
// (note that there is no sugar helper for this specific of an assertion)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);

// A snapshot will check that a mock was invoked the same number of times,
// in the same order, with the same arguments. It will also assert on the name.
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.getMockName()).toBe('a mock name');

전체 매처 목록을 보려면 [JEST 나아가기 3] 🔎Mock Functions 관련 매처🔎를 확인하자.
Mock Funtions Method가 궁금하다면 참조 문서를 확인하자.


이 글은 아래 사이트를 참고하여 작성했습니다🙂
https://jestjs.io/docs/mock-functions

profile
I'm jerry

0개의 댓글