mock은 사전적의미는 모조품입니다.
Jest에서는 테스트를 하기 위해 만들어 놓는 가짜 객체나 가짜 함수라고 생각하시면 됩니다.
아래와 같은 forEach라는 함수를 정의했다고 가정해봅시다.
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
forEach 함수에 들어가는 callback 함수를 테스트를 위한 용도로만 만들어 보겠습니다.
jest.fn으로 mock 함수를 만들 수 있습니다.
아래가 예제입니다.
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);
// 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함수는 mock 속성을 가집니다.
이 mock속성은 어떻게 함수가 호출되었는지 함수가 무엇을 반환했는지 저장하고 있습니다.
또한 mock속성은 this의 값을 추적합니다.
아래는 mock속성을 보여주는 예시입니다.
const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]
const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ]
더 자세히 알아봅시다.
// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(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');
// The function was called with a certain `this` context: the `element` object.
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');
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 할 수 있는 기능은 하위 기능을 구체적으로 알지 않고 반환 값만 보내도록 함으로써 테스팅의 복잡함을 줄일 수 있습니다.
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
아래와 같이 axios패키지를 이용해 api요청을 하는 클래스가 있다고 가정해 봅시다.
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
위의 함수를 테스트하면 되지만 매 테스트마다 실제로 API요청을 보내는 것은 부담스러울 수 있습니다.
이때는 jest.mock() 함수를 이용해 모듈을 mock으로 만들면 됩니다.
모듈을 mock하면 mockResolvedValue을 제공하는데 이 함수는 우리가 설정한 값을 반환하게 만듭니다.
위의 axios를 테스트 하기 위해 아래와 같이 할 수 있습니다.
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));
});
아래와 같은 모듈이 있다고 가정해봅시다.
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
모듈을 Import한 뒤 jest.mock함수를 이용해 부분적으로 모듈을 mock으로 만들 수 있습니다.
아래는 예시입니다.
//test.js
import defaultExport, {bar, foo} from '../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();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();
expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});
위에서 bar외에는 모두 mock으로 대체됩니다.
mock 함수의 구현 자체를 완전히 대체하는 것이 유용한 경우가 있습니다.
이때는 'jest.fn' 또는 mock함수의 'mockImplementationOnce' 함수를 이용하면 됩니다.
1. jest.fn 이용해서 구현
jest.fn은 완전히 새로운 mock을 구현할 수 있습니다.
const myMockFn = jest.fn(cb => cb(null, true));
myMockFn((err, val) => console.log(val));
// > true
module.exports = function () {
// some implementation;
};
이제 모듈을 mockImplementation을 통해 다른 함수로 대체해보겠습니다.
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
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 함수에서 this를 리턴할 때 두 가지 방법이 있습니다.
mockReturnThis함수를 이용하거나 직접 this를 리턴하게 하는 것입니다.
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};
테스트를 할 때 mock한 함수의 이름을 옵션으로 주어줄 수 있습니다.
mockName함수를 이용하면 됩니다.
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');