우테코에 참여하면서 의도치않게 테스트코드를 보았는데, jest
는 npm test
명령어만 써보고 직접 테스트코드를 짜본적은 없었어서
테스팅라이브러리인 jest
에 대한 사용법을 알아볼까합니다.
기본적인 사용법
기본적으로 jest의 test파일은 테스트할함수파일명.test.js
로 해줍니다.
jest 공식문서에서는 아래와 같은 예시를 보여주고 있습니다. (공식문서 + 예시추가)
sum.js
function sum(a,b){
return a + b;
}
module.exports = sum;
sum.test.js
const sum = require('./sum');
describe('계산테스트', () => {
const a = 1, b = 2;
test('a + b는 3이다.', () => {
expect(sum(a,b)).toEqual(3);
})
})
위의 예시만 보아도 쉽게 테스트를 이해할 수 있었는데요, describe
같은 경우에는 테스트 그룹을 묶어주는 역할을 합니다.
a
나 b
는 테스트에서 쓰일 가짜 변수이고, expect
에는 검증대상을 넣어줄 수 있습니다. 다음으로 오는 toBeXXX
에는 검증결과를 넣어 테스트를 돌려주면 됩니다.
toBeXXX
부분에서 사용되는 함수들을 Test Matcher
라고 합니다.
Jest
는 기본적으로 test.js
로 끝나거나, __test__
디렉터리 안에 있는 파일들은 모두 테스트 파일로 인식한다고 합니다.
mocking (모킹)
Jest
관련해서 서칭하면서 모킹이란 단어를 마주할 수 있었는데요, 저는 자꾸 미드를 많이 봐서 그런지 you moking me?
이런 말만자꾸 생각나더라구요😅
여기서 moking
이란 모조품이란 뜻 그대로 사용됩니다.
즉, 테스트하고자 하는 코드가 의존하는 함수나 클래스에 대해 모조품을 만들어서, 일단 돌아가게 만드는 것입니다.
왜 가짜로 대체하는 것일까요?
정답은 테스트하고 싶은 기능이 다른 기능들과 엮여있는 경우, 정확한 테스트를 하기 힘들기 때문입니다.
예를들어 request body
에 사용자의 아이디와 패스워드를 담아서 post요청을 보낸다고 가정해보겠습니다.
즉, 컨트롤러에서 정보를 추출한 후 데이터베이스에 넣어주는 단위테스트를 해본다고 가정해보겠습니다. (여기서 컨트롤러는 그냥 함수입니다.)
데이터 베이스에서 어떠한 응답이든 올 것이고, 반환된 응답을 기준으로 성공과 실패를 구분하게 됩니다.
하지만 이 과정은 좋은 방법이라고 할 수 없습니다.
- 만약 실패하는 경우, 컨트롤러 내부에 있는 로직때문인지 아니면 데이터베이스의 문제인지 판단하기 어렵게 됩니다.
- 실제 트랜잭션이 일어나는 IO시간도 테스트에 포함됩니다.
따라서 데이터베이스에 실제 데이터를 넣는게 아니라, 넣는 셈을 치자는 개념입니다.
데이터베이스가 잘 작동하는지는 데이터베이스 관련 테스트에서 확인하면 되고,
우리는 지금 컨트롤러에 대한 테스트를 진행하고 있으니 데이터베이스가 잘 작동한다는 전제를 깔고 가자는 뜻입니다.
기존의 데이터베이스 저장 메소드를 mock 함수로 만듭니다.
이제 이 아무 의미 없는 mock함수를 호출했을때 반환 받기 원하는 값을 우리가 직접 지정해 줍니다.
우리는 controller의 로직에 집중해야하니 데이터베이스는 "대충 이런이런 값을 반환한다고 치자"라고 하고 넘어가는 개념입니다.
Mocking 메소드 - jest.fn
Jest는 가짜 함수(mock functiton)를 생성할 수 있도록 jest.fn()
함수를 제공합니다.
이를 이용해서 일회성 테스트용으로서 내부의 함수를 진짜같이 구동해서 코드를 구동 시킬 수 있습니다.
jest.fn의 종류
- mockReturnValue(value)
const mockFn = jest.fn();
// 아래 코드는 모두 undefined, 기본적으로 빈 함수이기때문에 undefined를 출력합니다.
console.log(mockFn())
console.log(mockFn(1))
console.log(mockFn('2'))
console.log(mockFn([1,2,3]))
mock함수를 만들고 나서, 실행하면 undefined
가 출력됩니다. 따라서 함수의 리턴값을 지정할 수 있습니다.
const mockFn = jest.fn();
mockFn.mockReturnValue('다은짱짱만세');
const result = mockFn();
console.log(result) // '다은짱짱만세'
- mockImplemetation(value)
모크 함수는 기본적으로 비어있습니다. (아무런 동작, 리턴을 하지 않는다.)
mockImplemetation() 는 모크 함수를 즉석으로 구현할 수 있습니다. 동작하는 모크 함수를 만드는 것이라고 보면 됩니다.
const mockFn = jest.fn();
mockFn.mockImplemetation((name) => `I am ${name}!`);
console.log(mockFn('Daeun')) // I am Daeun!
// 다른 방법
const mockFn = jest.fn((name) => `I am ${name}!` );
console.log(mockFn("Daeun")); // I am Daeun!
단축 속성법으로 jest.fn()
함수 내부에 써서 똑같이 구현할 수 있습니다.
- mockResolvedValue(value) / mockRejectedValue(value)
위의 함수를 사용하면 가짜 비동기 함수를 만들 수 있습니다.
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.mockImplemetation((name) => `I am ${name}!`);
mockFn('Daeun');
mockFn(['Dale', 'James']);
mockFn.toBeCalledTimes(2); // 2번 호출되었니?
mockFn.toBeCalledWith('Daeun'); // 'Daeun'으로 호출된 적있니?
mockFn.toBeCalledWith(['Dale', 'James']); // 매개변수로 들어온 배열로 호출된 적 있니?
})
jest
를 공부하면서 콘솔과 같은 내장함수를 모킹할 순 없을까?란 생각이 들었는데요.
이번에 우테코 프리코스에 참여하면서 아래와 같이 모킹하는 방법을 알아내게 되었습니다.
const mockConsoleFn = (input) => {
MissionUtils.Console.readLineAsync = jest.fn();
MissionUtils.Console.readLineAsync.mockImplementation(()=> input);
};
위의 코드에서 MissionUtils.Console.readLineAsync
는 비동기적으로 사용자에게 입력값을 받아내는 함수입니다.
따라서 이 함수를 모킹하고, 매번 입력을 받아낼 때마다 input
의 값에 맞게 출력할 수 있습니다.
예를 들어 매개변수로 '1'을 넣게 된다면 매번 어떤 입력을 받아낼 때마다 '1'을 입력하게 되는 것입니다.
이처럼 모킹함수를 직접적으로 만들 수도 있고, 기존에 만들어져있는 함수를 가져와 사용할 수도 있습니다.
Mock function 테스트하기
Mock function은 아래와 같은 matcher
를 사용해서 호출한 횟수, 호출한 인수를 테스트할 수 있습니다.
expect(mockFn).toBeCalled()
: 1번 이상 호출되면 참toBeCalledTimes(3)
: 3번 이상 호출되면 참toBeCalledWith(10,20)
: 인수로 10과 20을 받은 함수가 있는지?lastCalledWith(30,40)
: 마지막으로 실행된 함수의 인수가 30, 40 인가?
Spy function 만들기
mocking에는 스파이(spy)라는 개념이 있습니다. 현실이나 영화 속에서 스파이라는 직업은 “몰래” 정보를 캐내야 합니다. 테스트를 작성할 때도 이처럼, 어떤 객체에 속한 함수의 구현을 가짜로 대체하지 않고, 해당 함수의 호출 여부와 어떻게 호출되었는지만을 알아내야 할 때가 있습니다. 이럴경우, spyOn
을 사용하여 해당 함수를 spy function으로 만든다.
jest.spyOn(object, methodName)
test('계산기 객체의 더하기 함수 테스트', () => {
// object
const calculator = {
add: (a, b) => a + b, // method
};
// calculator.add() method에 spy를 붙이기
const spyFn = jest.spyOn(calculator, 'add');
// Spy를 붙인 함수를 실행하면
const result = calculator.add(1, 2);
expect(spyFn).toBeCalledTimes(1); // 호출 횟수 테스트하기
expect(spyFn).toBeCalledWith(1, 2); // 호출된 인자 테스트하기
});
우테코 프리코스에서 저는 spyOn
함수를 콘솔기능을 담당하는 함수를 추적하고 싶었습니다.
그래서 원하는 바대로 콘솔에 출력이 되나? 확인을 해보고 싶었어서 아래와 같이 코드를 짜주었습니다.
const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print") // Console.log같은 기능 print = log
logSpy.mockClear(); // 각각의 시나리오를 구분하기 위해 호출 정보 재설정
return logSpy;
}
그래서 만약에 제가 원하는 바대로 콘솔로 에러를 내뱉고있는지 확인하기 위해서 mocking 함수와 spyOn
를 사용해서 아래와 같은 테스트 코드를 작성할 수 있었습니다.
const mockConsoleFn = (input) => {
// 사용자한테 입력을 받는 모킹함수
MissionUtils.Console.readLineAsync = jest.fn();
MissionUtils.Console.readLineAsync.mockImplementation(()=> input);
}
const getLogSpy = () => {
// 콘솔을 추적하는 spyOn
const logSpy = jest.spyOn(MissionUtils.Console, "print");
logSpy.mockClear();
return logSpy;
}
describe("게임 종료 및 재개 테스트", () => {
test("게임을 종료하는 경우", async () => {
const app = new App();
const logSpy = getLogSpy();
// 사용자가 입력하는 값을 무조건 2라는 문자로 받게 한다.
mockConsoleFn('2');
await app.restart();
// 사용자가 2를 했을 때, 게임이 끝나게 되는지 확인
expect(logSpy).toHaveBeenCalledWith(MESSAGE.END);
})
test("1과 2가 아닌 다른 값을 입력했을 경우", async () => {
const app = new App();
// 사용자가 입력하는 값을 무조건 게임종료해줘. 라는 문자로 받게 한다.
mockConsoleFn('게임종료해줘.');
// 비동기 에러 발생은 rejects.toThrow로 확인할 수 있다.
await expect(app.restart()).rejects.toThrow(ERROR_MESSAGE.RESTART);
})
})
Reference Doc
[JEST] 📚 JEST 소개 & 기본 사용법 정리
Jest의 jest.fn(), jest.spyOn()를 이용한 함수 모킹
[JEST] 📚 유용한 Matcher 함수 종류 모음
Jest 공식문서