Mock Functions 개요
- 실제 구현 된 함수를 Mock(모의) 함수로 대체하면 코드 간의 상호 작용이 쉽게 테스트 가능
- Mock 함수의 장점
- 호출 시 함수에 전달되는 인자 캡쳐 가능
- new를 통해서 생성할 때 인스턴스 캡쳐 가능
- 테스팅 과정 중 return 값 설정 가능
- Mock 함수의 2가지 사용 방법
- 테스트 코드에 사용하기 위해 mock 함수 생성
- module dependency를 override하기 위해 manual mock 작성
Mock function 사용
- 함수를 구현하고 해당 함수를 테스트하기 위해 mock 함수를 사용
예제 - 직접 구현 한 forEach 함수를 테스트
구현
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
테스트
- mock 함수 호출 횟수 체크
- mock 함수 호출 시 넘겨 받은 인자 값 체크
- mock 함수 호출의 결과 값 체크
describe('Test self implemented forEach function', () => {
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
test('The mock function is called twice', () => {
expect(mockCallback.mock.calls.length).toBe(2);
});
test('The first argument of the first call to the function was 0', () => {
expect(mockCallback.mock.calls[0][0]).toBe(0);
});
test('The first argument of the second call to the function was 1', () => {
expect(mockCallback.mock.calls[1][0]).toBe(1);
});
test('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 함수가 어떻게 호출 되었는지, 어떤 값을 return 했는지 등의 정보를 가지고 있음
- 매 호출 시 this 값도 가지고 있음
- mock.instances는 mock 함수로 부터
new
를 사용해 인스턴스화 된 객체를 배열로 가지고 있음
describe('mock property', () => {
const myMock = jest.fn(function (_name) {
this.name = _name;
});
test('track the value of this for each call', () => {
const a = new myMock('a function');
const b = {};
const bound = myMock.bind(b);
bound('b function');
expect(myMock.mock.instances[0]).toEqual(a);
expect(myMock.mock.instances[1]).toEqual(b);
});
})
Mock Return Values
- mock 함수의 return 값을 테스트 중간에 주입(inject) 가능
describe('test mock return values', () => {
const myMock = jest.fn();
expect(myMock()).toEqual(undefined);
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
test('test first call', () => {
expect(myMock()).toBe(10);
});
test('test second call', () => {
expect(myMock()).toBe('x');
});
test('test third call', () => {
expect(myMock()).toBeTruthy();
});
test('test fourth call', () => {
expect(myMock()).toBeTruthy();
});
});
- 연속적으로 값을 전달하는 형태의 코드에서 효율적으로 사용 가능
- 복잡하게 실제 함수를 수정할 필요 없이, 직관적으로 값을 지정할 수 있음
- mock.calls는 mock 함수 호출 시 전달 된 인자 값들을 담고 있는 배열
describe('continuation-passing style', () => {
const filterTestFn = jest.fn();
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(num => filterTestFn(num));
test('test result', () => {
expect(result).toEqual([11]);
});
test('test call arguments', () => {
expect(filterTestFn.mock.calls).toEqual([[11], [12]]);
});
});
Mocking Modules
- 모듈 내부의 특정 메소드의 동작을 모의로 만듦
- mock async function을 만들 때 유용하게 사용
실제 구현 된 API 호출 모듈
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
mock으로 구현 된 모듈을 이용한 테스트
import axios from 'axios';
import Users from './users';
jest.mock('axios');
describe('Mocking Modules', () => {
test('should fetch users', async () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
const result = await Users.all();
expect(result).toEqual(users);
});
});
Mock Implementations
함수 전체를 대체
- 아직 구현이 안되어 있거나 내부 구현이 복잡하여 테스트 하기 힘든 경우
- 테스트를 위해 간단한 구현체를 만들어서 해당 함수를 대체
- jest.fn, mockImeplementation을 사용하여 구현
- jest.mock으로 mocking 하겠다고 지정하게 되면, 실제 구현체를 가려지고 mock 구현체를 바라보게 됨
export default () => {
console.log('Actual implementation');
return 42;
}
jest.mock('../src/foo');
import foo from '../src/foo';
describe('Mock Implementations', () => {
test('test jest fn', () => {
const myMockFn = jest.fn(cb => cb(null, true));
expect(myMockFn((err, val) => val)).toBeTruthy();
});
test('test mock implementation', () => {
foo.mockImplementation(() => {
console.log('Mock implementation');
return 77;
});
expect(foo()).not.toBe(42);
expect(foo()).toBe(77);
});
});
한 번의 호출에 대한 구현만 대체
- 다양한 경우를 테스트 하는 경우
- 호출 마다 다른 결과 값을 return 하도록 구현 가능
- mockImplementationOnce로 구현
- 한 번의 호출에 대한 구현을 지정할 수 있고, 지정 된 구현이 더 이상 없다면 jest.fn에 지정 한 기본 구현을 사용. 기본 구현이 없다면
undefined
return
describe('Mock Implementation Once', () => {
test('test mockImplementationOnce without default', () => {
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
const cbFn = (err, val) => val;
expect(myMockFn(cbFn)).toBeTruthy();
expect(myMockFn(cbFn)).toBeFalsy();
expect(myMockFn(cbFn)).toBeUndefined();
});
test('test mockImplementationOnce with default', () => {
const myMockFn = jest
.fn(cb => cb(null, 'default'))
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
const cbFn = (err, val) => val;
expect(myMockFn(cbFn)).toBeTruthy();
expect(myMockFn(cbFn)).toBeFalsy();
expect(myMockFn(cbFn)).toBe('default');
});
});
체이닝을 위해 this를 return하는 경우
describe('Mock Return This', () => {
test('test mockReturnThis', () => {
const myObj = {
myMethod: jest.fn().mockReturnThis(),
log: jest.fn(() => 'logging')
};
expect(myObj.myMethod()).toEqual(myObj);
expect(myObj.myMethod().log()).toBe('logging');
});
});
Mock Names
- mock 함수에 이름을 지정하면 에러 발생 시 좀 더 명확하게 알 수 있음
describe('Mock Names', () => {
test('test display mock function name', () => {
const myMockFn = jest
.fn()
.mockName('namedMockFn')
expect(myMockFn).toHaveBeenCalled();
});
});
Custom Matchers
- .mock 프로퍼티를 통해서 직접 구현이 가능하지만, 조금 더 사용하기 편하게 제공되는 matcher
describe('Custom Matchers', () => {
const mockFunc = jest.fn();
const arg1 = 1;
const arg2 = 2;
test('The mock function was called at least once', () => {
mockFunc();
expect(mockFunc).toHaveBeenCalled();
});
test('The mock function was called at least once with the specified args', () => {
mockFunc(arg1, arg2);
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
});
test('The last call to the mock function was called with the specified args', () => {
mockFunc(3, 4);
mockFunc(arg1, arg2);
expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
});
test('All calls and the name of the mock is written as a snapshot', () => {
mockFunc(arg1, arg2);
expect(mockFunc).toMatchSnapshot();
});
});