Dog foot print

[JEST] Mock functions 본문

TDD/JEST

[JEST] Mock functions

개 발자국 2021. 5. 19. 17:00

Mock 함수는 테스트에서 사용 할 수 없는 함수(window.replace)와 함수에 어떤 파라메터가 전달되었는지, 인스턴스가 new 키워드로 생성되었을 때 생성자 함수를 캡쳐링 하는 것을 가능하게 도와준다.

Mock 함수를 사용하는 방법은 두개가 존재한다. 이 두 방법은 test code에서 직접 Mock 함수를 생성하거나, 의존성 모듈에 직접 mock함수를 작성하는 방법이다.

Using a mock function

다음과 같이 forEach 함수를 통해서 배열을 순회하며, 전달 된 callback으로 배열 내부에 아이템을 전달한다고 생각해보자.

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

이 테스트를 실행하기 위해 우리는 mock함수를 이용 하여, forEach에 전달된 callBack 함수의 상태를 확인 할 수 있다.

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

//callBack으로 전달된 함수가 몇번 실행 되었는가 ? 
expect(mockCallback.mock.calls.length).toBe(2);

//첫번째로 실행된 callBack함수의 1번째 전달인자를 확인하는 방법 . 
expect(mockCallback.mock.calls[0][0]).toBe(0);

//두번째로 실행된 callBack함수의 1번째 전달인자를 확인하는 방법 . 
expect(mockCallback.mock.calls[1][0]).toBe(1);

//첫번째 전달된 콜백의 return 값은 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의 프로퍼티들은 함수 호출, 반환, 인스턴스화 되었을 때 테스트하기 매우 유용합니다.

//이 함수는 한번만 실행되었습니다.
expect(someMockFunction.mock.calls.length).toBe(1);

//콜백이 처음 실행 되었을 때 첫번째 전달인자는 'first arg'입니다.
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');

//함수가 처음 실행 되었을 때 2번째 전달인자는 'second arg'입니다.
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

//할수가 처음 실행 되었을 때 반환된 값은 'return value'입니다. 
expect(someMockFunction.mock.results[0].value).toBe('return value');

//이 함수는 정확히 2번 인스턴스화 되었습니다. 
expect(someMockFunction.mock.instances.length).toBe(2);

//첫번째 인스턴스의 name 프로퍼티는 'test'입니다. 
expect(someMockFunction.mock.instances[0].name).toEqual('test');

Mock Return values

Mock 함수는 테스트 도중 값을 설정 할 수 있습니다.

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

Mock 함수는 forEachfilter 와 같이 지속적으로 콜백함수가 호출 되는 스타일에서 매우 유용합니다. 이런 스타일로 작성된 테스트는 콜백으로 전달되는 함수를 직접 구상하고 동작을 재현하지 않아도 되어, 복잡함을 제거하기 매우 유용한 방법 입니다.

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);
// > [ [11], [12] ]

많은 실제 예제에서는 의존하는 컴포넌트와 이를 구성하는 설정에 대하여, Mock 함수를 통해 파악하고, 이를 구성 하지만, 이를 직접 만드는 것은 되도록 피하세요 ! .

Mocking Modules

우리가 유저의 정보를 가져올 수 있는 API를 가진 Class를 가지고 있다고 가정 하겠습니다. 이 클래스는 axios(자바스크립트 통신 라이브러리)를 통해서, user의 데이터 정보를 리턴합니다 .

// 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 을 만들 수 있습니다.

우리는 axios모듈의 get 메서드가 우리가 원하는 test에 반환값을 전달하기 위해 미리 mockResolvedValue 를 이용 해 한번 지정해야 합니다. 즉 axios.get('/users.json')의 가짜 응답을 만들어야 합니다.

import axios from 'axios';
import Users from './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));
});

Note : 위의 Users 코드에서는 axios를 사용하지만 test 코드에서 우리가 jest.mock으로 만든 가짜 axios가 사용됩니다.

Mock Implementations

값을 구체적으로 지정하는 것을 넘어, 모의 함수 구현을 완전히 대체하는 것이 유용한 경우가 있습니다. 이것은 mock 함수의 jest.fn혹은 mockImplementationOnce 를 사용하는 것으로 가능합니다.

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

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

mockImplementation 메서드는 다른 모듈을 대상으로 하는 기본 모의 함수를 정의 할 때 무척이나 유용합니다.

// foo.js
module.exports = function () {
  // some implementation;
};

// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 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));

만약 mock 함수가, 우리가 지정한 mockImplementationOnce 보다 더 많이 실행된다면, undefined를 반환하게 됩니다. 이 경우, 테스트를 위해 기본 값을 지정 해 줄 수 있습니다.

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

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

우리가 사용하는 몇개의 메서드는 체인으로 연결되어 있습니다. (이 경우 this를 반환함) 우리는 아주 좋은 API인 .mockReturnThis 함수를 사용하는 것으로 해결 할 수 있습니다.

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

// is the same as

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

Mock Names

Mock 함수를 이용한 테스트를 실행하며, error가 발생하는 경우, 제스트는 jest.fn함수에서 에러가 발생했다고 표시됩니다. 그렇기에 중첩된 mock 함수들이 존재하는 경우, 문제를 찾기 어렵습니다. .mockName메서드를 체인으로 연결하면, 더이상 jest.fn이 아닌 우리가 지정해준 이름으로 에러를 찾을 수 있습니다.

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

Custom Matchers

마지막으로 mock함수 또한 해당 함수가 어떻게 호출 되었는지 파악하기 위해 몇가지 custom matcher를 추가 하였습니다.

// 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();

Matcher들은 .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');
반응형

'TDD > JEST' 카테고리의 다른 글

[JEST]snapshot test  (0) 2021.05.24
[JEST] Setup and  Teardown  (0) 2021.05.18
[JEST] 비동기 코드 테스트  (0) 2021.05.16
[JEST] Matcher 사용하기  (0) 2021.05.16
[JEST] 시작하기 설치 및 설정  (1) 2021.05.15
Comments