이번에야말로 모킹을 통해 유닛테스트를 작성해보자.
왜 필요한가?
솔직히 이걸 제대로 이해하지 못해서 계속 모킹이라는 것을 이해하지 못했던 것 같다.
이전에 적었던 글을 통해 테스트환경을 구성하고, 도커 컨테이너를 띄워 실제 DB를 만든다음 여기에 CRUD를 하며 서버의 코드를 테스트했었다.
그런데 멘토링을 진행하다가 이것이 유닛테스트가 아니라는 걸 알게되었다. 내가 구성한건 통합테스트 환경에 가까웠다.
분명 이론상으로는 알고있었지만, 제대로 분류를 해볼 생각은 못했던 것이다.
기존의 테스트 코드 예제를 한번 보고,Controller코드 테스팅을 한다고 생각해보자.
add.test.ts
const add = (a: number, b: number) => a + b;
test('add Test1', () => {
expect(add(1, 2)).toBe(3);
});
test('add Test2', () => {
expect(add(3, 4)).toBe(7);
});
test('add Test3', () => {
expect(add(6, 2)).toBe(8);
});
간단하다. 1,2를 더하면 3이 나와야 하니 첫 테스트가 저렇게 적혔고,
3,4가 더해지면 7이 나와야 하므로 두번째 테스트가 저렇게 나왔다.
마찬가지로 6과 2가 더해지면 8이므로 저렇게 테스트를 작성했다.
자 그럼 이번에는 서버단의 Controller를 테스트한다고 생각해보자.
아래는 소셜로그인을 돕는 CalbackController이다.
CallbackController.post를 테스트해보자.
export class CallbackController {
static async post(req: Request, res: Response) {
const { accessToken, platform } = req.body;
if (platform === 'google') {
const data = await CallbackService.google(accessToken);
const tokens = await LoginService.login(data);
return res.json(tokens);
}
if (platform === 'kakao') {
const data = await CallbackService.kakao(accessToken);
const tokens = await LoginService.login(data.kakao_account);
return res.json(tokens);
}
throw new ErrorStatus('잘못된 요청입니다', 400);
}
}
CallbackController.post는 req와 res를 인자로 받고, res.json()을 return한다.
이 함수를 테스트하려면 문제가 발생한다.
1. 각종 의존성 (CallbackService.google, LoginService.login, CallbackService.kakao)
2. res.json은 void를 반환함
이 두가지 문제를 해결 할 수 있는 것이 mock함수이다.
현재 테스트의 목적은 Controller가 제대로 작동하는가? 이다.
따라서 처리된 값이 제대로 함수로 전달되고, 결과가 잘나오는가? 를 확인해야한다.
따라서 서비스 로직이 실제로 동작할 필요가 없다.
따라서 각종 의존성을 Mock하여 테스트한다.
Mocking을 하게되면 실제 로직에서 해당 함수가 호출되었을때, 실제 함수 대신에 우리가 만든 가짜함수가 실행된다.
이렇게 Mocking된 함수는 어떻게 호출되었는지와 어떤 값을 반환할지를 정해줄 수 있다.
어떻게 호출되었는지를 아는 것이 함수를 추적할 수 있다는 것이므로 좀 중요한 것 같다.
Mocking하기
그럼 이제 진짜 모킹을 해보자.
jest.SpyOn(객체, 메서드)
jest.spyOn은 객체의 메서드를 Mocking한다.
이렇게 하면 해당 객체의 메서드가 Mock함수가 된다.
이를 통해 실제 그 메서드가 로직에서 호출됐을때의 return Value, resolve된 Value를 결정해줄 수 있고
호출여부와 어떤 인자를 통해 호출되었는지를 확인할 수 있다.
값 정해주기
1. jest.spyOn(객체, 메서드).mockReturnValue
mockReturnValue를 통해 함수의 어떤 값이 return 될지를 결정해줄 수 있다.
2. jest.spyOn(객체, 메서드).mockResolvedValue
mockResolvedValue를 통해 Promise를 반환하는 비동기 함수의 resolve값을 정해줄 수 있다.
그럼 이제 CallbackService.google을 모킹해보자.
//CallbackController.test.ts
jest.spyOn(CallbackService, 'google').mockResolvedValue('Promise가 Resolve된 값이 뭐가 나올지');
이제 아래 로직을 다시 보자
export class CallbackController {
static async post(req: Request, res: Response) {
const { accessToken, platform } = req.body;
if (platform === 'google') {
const data = await CallbackService.google(accessToken); // 설정해준 resolved Value가 data에 담김
const tokens = await LoginService.login(data);
return res.json(tokens);
}
if (platform === 'kakao') {
const data = await CallbackService.kakao(accessToken);
const tokens = await LoginService.login(data.kakao_account);
return res.json(tokens);
}
throw new ErrorStatus('잘못된 요청입니다', 400);
}
}
비슷하게 LoginService.login도 모킹해주자.
//CallbackController.test.ts
jest.spyOn(CallbackService, 'google').mockResolvedValue('Promise가 Resolve된 값이 뭐가 나올지');
jest.spyOn(LoginService, 'login').mockResolvedValue('토큰이 대충 이렇게 나오지 않을까요?');
export class CallbackController {
static async post(req: Request, res: Response) {
const { accessToken, platform } = req.body;
if (platform === 'google') {
const data = await CallbackService.google(accessToken); // 설정해준 resolved Value가 data에 담김
const tokens = await LoginService.login(data); //tokens에 대충 토큰이 담김
return res.json(tokens);
}
...
}
오케이, 여기까지 문제없이 잘 수행했다.
문제는 res.json이다. 어떻게 해야할까?
jest.fn()을 통해 모킹하여 해결해보자.
jest.fn()
jest.fn는 아무일도 하지 않는 함수를 만든다.
아무 일도 하지 않는걸 왜 만듦?
이라고 할 수 있지만, 중요한건 함수 호출시에 들어온 인자와 반환값이 추적가능하다는 점이다.
res객체를 이렇게 구성해주자.
express의 Response는 뭐가 많아서 대충 any로 구성했다.
// CallbackController.test.ts
const res: any = {
json: jest.fn(),
};
이러면 res.json은 아무일도 하지 않는 함수가 된다.
대신, 로직에서 res.json이 호출될때, 실제 res.json대신 우리가 위에서 만든 가짜 함수가 실행되어, 들어온 인자를 확인할 수 있게 만들어 줄 것이다.
함수 호출시의 인자 tracking하기
자 이제 모킹된 함수의 인자를 추적해보자.
expect(CallbackService.google).toHaveBeenCalledWith('CallbackService.google이 호출될때 인자로 들어갈 녀석');
toHaveBeenCalledWith를 통해 mock된 함수가 어떤 인자를 통해 호출되었는지 확인할 수 있다.
res.json도 마찬가지다.
expect(res.json).toHaveBeenCalledWith('res.json()호출시 들어간 인자값');
전체 테스트 코드
import { Request, Response } from 'express';
import { CallbackService } from '../../services/CallbackService';
import { LoginService } from '../../services/LoginService';
import { CallbackController } from '../CallbackController';
describe('callback Controller', () => {
it('google', async () => {
const req: any = {
body: {
accessToken: 'googleAccessToken',
platform: 'google',
},
};
const res: any = {
json: jest.fn(),
};
const mockGoogleData = {
email: 'gildong@naver.com',
name: 'gildong',
picture: 'https://~',
};
const mockTokenData = {
access: 'asdf',
refresh: 'asdf',
};
jest.spyOn(CallbackService, 'google').mockResolvedValue(mockGoogleData);
jest.spyOn(LoginService, 'login').mockResolvedValue(mockTokenData);
await CallbackController.post(req as Request, res as Response);
expect(CallbackService.google).toHaveBeenCalledWith('googleAccessToken');
expect(LoginService.login).toHaveBeenCalledWith(mockGoogleData);
expect(res.json).toHaveBeenCalledWith(mockTokenData);
});
참고한 글
https://goonerholic.github.io/express-testing
https://www.daleseo.com/jest-fn-spy-on/
https://dev.to/dylanju/jest-mocks-18l9