른록노트

[Javascript] Jest - Mock Functions 본문

Programming/[Javascript]

[Javascript] Jest - Mock Functions

른록 2021. 12. 5. 03:14

Mock Functions

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

Mock 함수에는 두 가지 방법이 있습니다. 테스트 코드에서 사용할 Mock 함수를 생성하거나 모듈 종속성을 재정의하기 위해 manual mock을 작성하는 것입니다.

Using a mock function

제공된 배열의 각 항목에 대한 콜백을 호출하는 forEach 함수의 구현을 테스트한다고 가정해 보겠습니다.
이 함수를 테스트하기 위해 mock 함수를 사용하고 mock 상태를 검사하여 예상대로 콜백이 호출되는지 확인할 수 있습니다.

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

test('mock test', () => {
  const mockCallback = jest.fn((x) => 42 + x);
  forEach([0, 1], mockCallback);

  // The mock function is called twice
  expect(mockCallback.mock.calls.length).toBe(2);

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

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

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

.mock property

모든 mock 함수에는 이 특별한 .mock 속성이 있습니다. 이 속성에는 함수가 호출된 방법과 반환된 함수에 대한 데이터가 보관됩니다. .mock 속성은 또한 각 호출에 대해 this 값을 추적하므로 이를 검사할 수도 있습니다.

const myMock = jest.fn();

const a = new myMock();
const b = {};
const bound = myMock.bind(b);
bound();

console.log(myMock.mock.instances);
// > [ <a>, <b> ]

이러한 mock 멤버는 테스트에서 이러한 함수가 어떻게 호출되고, 인스턴스화되고, 반환되는지 확인하는 데 매우 유용합니다.

test('mock test2', () => {
  const someMockFunction = jest.fn((x) => {
    return 'return value';
  });
  const test = new someMockFunction('first arg', 'second arg');
  test.name = 'test';

  // The function was called exactly once
  expect(someMockFunction.mock.calls.length).toBe(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');

  const test2 = new someMockFunction('test', 'second arg');
  // 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).toEqual('test');
});

Mock Return Values

mock 함수는 테스트 중에 코드에 테스트 값을 주입하는 데 사용할 수도 있습니다.

test('mock test3', () => {
  const myMock = jest.fn();
  console.log(myMock());
  // > undefined

  myMock
    .mockReturnValueOnce(10)
    .mockReturnValueOnce('x')
    .mockReturnValue(true)
    .mockReturnValue(false);

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

mock 함수는 functional continuation-passing 스타일을 사용하는 코드에서도 매우 효과적입니다. 이 스타일로 작성된 코드는 사용 직전에 테스트에 값을 직접 주입하기 위해 실제 구성 요소의 동작을 재현하는 복잡한 스텁의 필요성을 피하는 데 도움이 됩니다.

test('mock test4', () => {
  const filterTestFn = jest.fn();

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

  const result = [11, 12, 13].filter((num) => filterTestFn(num));

  console.log(result);
  // > [11,13]
  console.log(filterTestFn.mock.calls[0][0]); // 11
  console.log(filterTestFn.mock.calls[1][0]); // 12
  console.log(filterTestFn.mock.calls[2][0]); // 13
  expect(filterTestFn.mock.results[0]).toEqual({type: 'return', value: true});
  expect(filterTestFn.mock.results[1]).toEqual({type: 'return', value: false});
  expect(filterTestFn.mock.results[2]).toEqual({type: 'return', value: true});
});

대부분의 실제 예제에는 실제로 종속 구성 요소의 mock 함수를 파악하고 구성하는 것이 포함되지만 기술은 동일합니다. 이러한 경우 직접 테스트되지 않는 함수 내부에 논리를 구현하려는 유혹을 피하십시오.

Mocking Modules

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

users.js
---
import axios from 'axios';

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

export default Users;

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

모듈을 mock하면 테스트에서 데이터를 반환하는 .get에 대해 mockResolvedValue를 제공할 수 있습니다.
실제로 요청하는게 아니라 미리 반환값을 정해놔서 테스트 할 수 있습니다.

users.test.js
---
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 the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

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

Mocking Partials

모듈의 하위 집합은 mock 될 수 있고 나머지 모듈은 실제 구현을 유지할 수 있습니다.
(저는 홈페이지와 다르게 commonjs 방식을 사용했습니다)

foo-bar-baz.js
---
const foo = 'foo';
function bar() {
  return 'bar';
}
function baz() {
  return 'baz';
}
module.exports = {foo: foo, bar: bar, default: baz};
test.js
---
const defaultExport = require('./foo-bar-baz');
const {foo} = require('./foo-bar-baz');
const {bar} = 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 {
    __esModule: true,
    ...originalModule,
    default: jest.fn(() => 'mocked baz'),
    foo: 'mocked foo',
  };
});

test('should do a partial mock', () => {
  const defaultExportResult = defaultExport.default();
  expect(defaultExportResult).toBe('mocked baz');
  expect(defaultExport.default).toHaveBeenCalled();

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

Mock Implementations

그래도 반환 값을 지정하고 mock 함수 구현을 완전히 대체하는 기능이 유용한 경우가 있습니다. 이것은 jest.fn 또는 mock 함수의 mockImplementationOnce 메소드를 사용하여 수행할 수 있습니다.

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

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

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

foo.js
---
module.exports = function () {
  // some implementation;
};
test.js
---
jest.mock('./foo'); // this happens automatically with automocking
const foo = require('./foo');

test('mock impl test', () => {
  // foo is a mock function
  foo.mockImplementation(() => 42);
  expect(foo()).toBe(42);
  // > 42
});

여러 함수 호출이 다른 결과를 생성하도록 mock 함수의 복잡한 동작을 다시 만들어야 하는 경우 mockImplementationOnce 메서드를 사용하십시오.

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

test('mock impl 2', () => {
  myMockFn((err, val) => expect(val).toBeTruthy());
  // > true

  myMockFn((err, val) => expect(val).not.toBeTruthy());
  // > false
});

mock 함수가 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'

일반적으로 연결되어 있는(따라서 항상 이것을 반환해야 하는) 메서드가 있는 경우, 모든 mock 객체에 있는 .mockReturnThis() 함수의 형태로 이를 단순화하는 멋진 API가 있습니다.

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

// is the same as

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

Mock Names

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

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

Custom Matchers

마지막으로, mock 함수가 어떻게 호출되었는지 주장하는 것을 덜 요구하도록 하기 위해 몇 가지 사용자 정의 matcher 함수를 추가했습니다.

// mock 함수는 한 번 이상 호출되었습니다.
expect(mockFunc).toHaveBeenCalled();

// mock 함수는 지정된 인수로 한 번 이상 호출되었습니다.
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);

// mock 함수에 대한 마지막 호출이 지정된 인수로 호출되었습니다.
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);

// 모든 호출과 mock 이름은 스냅샷으로 작성됩니다.
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');

참고사이트

공식사이트 Mock Functions v27.2

진행사항

### 초급

1.  공식 홈페이지에서 개념정리
2.  공식 홈페이지에서 튜토리얼 or 가이드 실습하기 (진행)

### 중반

1.  실제로 프로젝트 만들기
2.  프로젝트 진행하면서 API DOC 찾아보며 정리하기

### 후반

1.  오픈소스 컨트리뷰트
반응형
Comments