mocking이란 (mock = 모조품) 뜻 그대로 받아드리면 된다.
즉 테스트하고자 하는 코드가 의존하는 function이나 class에 대해 모조품을 만들어 '일단' 돌아가게 하는 것이다.
한마디로, 단위 테스트를 작성할 때, 해당 코드가 의존하는 부분을 가짜(mock)로 대체하는 기법을 말한다.
왜 가짜로 대체하는가?
- 테스트 하고싶은 기능이 다른 기능들과 엮여있을 경우(의존) 정확한 테스트를 하기 힘들기 때문이다.
📣 예를 들어, POST 요청을 받았을 때 받은 id, password 값을 데이터베이스에 넣어주는 단위 테스트를 진행하고자 한다.
직접 데이터베이스에 접근해 그 값을 읽어 오는 것은 테스트 동작을 느리게 하며, 테스트 자체가 다른 네트워크 환경에 의존하고 있으므로 동일한 결과값을 보장받지 못 할 가능성이 있다.
따라서 실제 데이터베이스에 접근해 받아올 수 있는 예상값을 mock 함수를 통해 생성해 주고 이를 통해 단위 테스트를 진행 할 수 있다.
가짜 함수(mock functiton)
를 생성할 수 있도록 jest.fn() 함수를 제공한다.// 변수를 mock함수로 만들기
const mockFn = jest.fn();
// mock는 빈 함수이기 때문에 기본적으로 undefined
mockFn(); // undefined
mockFn(1); // undefined
mockFn([1, 2], { a: "b" }); // undefined
// mock 리턴값 지정하기
mockFn.mockReturnValue("I am a mock!"); // I am a mock
.mockReturnValue()
는 함수가 호출될 때마다 반환 값을 정한다.
const mock = jest.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(63);
mock(); // 63
const mockFn = jest.fn();
// 동작하는 모크 함수를 하나 만든다.
mockFn.mockImplementation( (name) => `I am ${name}!` );
console.log(mockFn("Dale")); // I am Dale!
jest.fn() 함수에 콜백을 통해서도 똑같이 구현할 수 있다.
const mockFn = jest.fn( (name) => `I am ${name}!` );
console.log(mockFn("Dale")); // I am Dale!
test('async resolve test', async () => {
const asyncMock = jest.fn().mockResolvedValue(43);
await asyncMock(); // 43
});
test('async reject test', async () => {
const asyncMock = jest.fn().mockRejectedValue(new Error('Async error'));
await asyncMock(); // throws "Async error"
});
test("mock Test", () => {
const mockFn = jest.fn();
mockFn.mockImplementation(name => `I am ${name}`);
mockFn("a");
mockFn(["b", "c"]);
expect(mockFn).toBeCalledTimes(2); // 몇번 호출? -> 2번
expect(mockFn).toBeCalledWith("a"); // a로 호출? -> true
expect(mockFn).toBeCalledWith(["b", "c"]); // 배열로 호출? -> true
})
📣 check 함수는 predicate()을 호출하였을 때 onSuccess의 인자로 ('yes')를 받아 실행시키고 그렇지 않을 경우는 OnFail 인자로 'no'를 받아 실행한다.
beforEach를 통해 각 테스트마다 onSuccess, onFail 모킹 함수를 만들어 준다.
테스트
1. 인자값으로 true를 받은 경우 if 조건이 만족하기 때문에 onSuccess 함수의 인자로 'yes'를 전달받아 1번 실행된다.
2. 인자값으로 false를 받은 경우 else 조건이 만족하기 때문에 onFail 함수의 인자로 'no'를 전달받아 1번 실행된다.
(onSuccess함수는 호출되지 않는다.)
check.jest.js
// mock 함수를 통해 함수의 호출 횟수, 인자를 테스트
const check = require('../check');
describe('check', () => {
let onSuccess;
let onFail;
beforeEach(() => {
onSuccess = jest.fn();
onFail = jest.fn();
});
it('should call onSuccess when predicate is true', () => {
check(() => true, onSuccess, onFail);
// onSuccess 함수의 호출 횟수
// expect(onSuccess.mock.calls.length).toBe(1);
expect(onSuccess).toHaveBeenCalledTimes(1);
// Onsuccess 함수의 인자
// expect(onSuccess.mock.calls[0][0]).toBe('yes');
expect(onSuccess).toHaveBeenCalledWith('yes');
// onFail 함수의 호출 횟수
// expect(onFail.mock.calls.length).toBe(0);
expect(onFail).toHaveBeenCalledTimes(0);
});
it('should call onFail when predicate is false', () => {
check(() => false, onSuccess, onFail);
expect(onFail).toHaveBeenCalledTimes(1);
expect(onFail).toHaveBeenCalledWith('no');
expect(onSuccess).toHaveBeenCalledTimes(0);
});
});
check.js
function check(predicate, onSuccess, onFail) {
if (predicate()) {
onSuccess('yes');
} else {
onFail('no');
}
}
module.exports = check;
📣 UserService.login() 을 실행하였을 때 인자(의존성)로 전달받은 userClient를 바탕으로 isLogedIn의 상태, 즉 로그인 상태를 변경한다.
그렇기 때문에 UserClient가 리턴하는 값을 mock 함수로 생성하여 테스트를 진행한다.
- UserClient 함수를 모킹()
jest.mock('../user_client');
- 그 리턴 값을 임의의 값인 'success'로 정해준다.
const login = jest.fn(async () => 'success'); UserClient.mockImplementation(() => { return { login, }; });
- beforeEach 를 통해 각 테스트마다 새로운 UserService를 만들어준다.
beforeEach(() => { userService = new UserService(new UserClient()); })
test
userService.login 함수가 호출되었을 때, 모킹된 UserClient가 1번 호출되어야 한다.
userService.login 함수가 2번 호출되는 경우, 모킹된 UserClient가 1번 호출되어야 한다.
(1번 호출되었을 때 this.isLogedIn 상태가 true로 바뀌기 때문에, 2번 호출되었을 때부터는 조건에 충족되지 않아 호출이 일어나지 않는다.)
user_service.test.js
const UserService = require('../user_service.js');
const UserClient = require('../user_client.js');
jest.mock('../user_client');
describe('UserService', () => {
const login = jest.fn(async () => 'success');
UserClient.mockImplementation(() => {
return {
login,
};
});
let userService;
beforeEach(() => {
userService = new UserService(new UserClient());
// login.mockClear();
// UserClient.mockClear();
})
it('calls login() on UserClient when tries to login', async () => {
await userService.login('abc', 'abc');
expect(login.mock.calls.length).toBe(1);
});
it('should not call login() on UserClient again if already logged id', async () => {
await userService.login('abc', 'abc');
await userService.login('abc', 'abc');
expect(login.mock.calls.length).toBe(1);
})
})
user_service.js
class UserService {
constructor(userClient) {
this.userClient = userClient;
this.isLogedIn = false;
}
login(id, password) {
if (!this.isLogedIn) {
// return fetch('http://example.com/login/id+password')
// .then((response) => response.json());
return this.userClient
.login(id, password)
.then((data) => (this.isLogedIn = true));
}
}
}
module.exports = UserService;
참조