nodejs를 통해 애플리케이션을 개발하며 테스트를 위해 Jest 프레임워크를 사용했다.
그런데 테스트 코드 작성이 처음이기도 해서 그런지 "mock"이라는 개념에서 많이 헤맸다.
첫 테스트 코드 작성이기에 다른 블로그의 글 보다 질 좋은 정보는 없겠지만 내가 어떻게 mock을 활용하여 단위/통합 테스트를 진행했는지 나중의 나를 위해 기록해놓는다.
1. Jest란
페이스북에서 개발한 JS를 위한 테스트 프레임워크이다.
npm install --save-dev jest
이렇게 설치해 주고 사용하자. --save-dev의 의미를 모르겠다면 마침 엊그제 쓴 좋은 글이있다. ㅎㅎ
https://llshl.tistory.com/40?category=961696
설치를 마치고 package.json의 script에
"scripts": {
...
"test": "jest",
...
},
이렇게 한 줄 넣어주자. 이제 우린 npm test라는 명령을 통해 테스트를 할 수 있게 됐다.
2. mock이란
기본적인 테스트 방법은 다른 블로그들이 아주 자세히 설명하고 있으니 여기서는 굳이 기록하지 않을려 한다.
"목", "목하다", "모킹하다"라고 표현하는 mock이란 간단히 말해서 "가짜로 대체하기" 기능이라고 말할 수 있다.
왜 가짜로 대체하는가?
- 테스트 하고싶은 기능이 다른 기능들과 엮여있을 경우(의존) 정확한 테스트를 하기 힘들기 때문
예를들어 request body에 사용자의 id와 password를 넣어서 post요청을 보내면 컨트롤러에서 정보를 추출한 후 데이터베이스에 넣어주는 단위테스트를 하고 싶다고 하자.
데이터베이스에 저장 요청을 보내면 성공이든 실패든 응답이 반환될 것이고 반환된 응답을 기준으로 테스트의 성공과 실패를 구분한다.
이때 실제 데이터베이스에 사용자의 id, password를 넣는 방식으로 테스트를 하는 것은 좋은 방법이 아니다.
실제 트랜잭션이 일어나기에 IO 시간도 테스트에 포함되고, 데이터베이스 연결 상태에 따라 테스트가 실패할 수도 있기 때문이다.
테스트가 실패했을 경우 내가 작성한 컨트롤러 코드의 문제인지, 데이터베이스의 문제인지 알아차리기도 힘들기 때문에 올바른 단위테스트라고 할 수 없다.
따라서 실제 데이터베이스에 데이터를 넣는 것이 아니라 넣은 셈 치자는 개념이다.
데이터베이스가 잘 작동하는지는 데이터베이스 관련 테스트에서 확인하면 되고 우리는 지금 controller에 대한 테스트를 진행하고 있으니 데이터베이스가 잘 작동한다는 전제를 깔고 가자는 뜻이다.
데이터베이스 mocking을 표현하면 다음 그림과 같다.
기존의 데이터베이스 저장 메소드를 mock 함수로 만든다. mock 함수가 되면 그 함수는 아무런 기능이 없어진다. mock 함수를 호출하면
undefined를 반환한다. 그냥 껍데기만 남기고 속을 싹 비워낸 그런 느낌
이제 이 mock함수를 호출했을때 반환 받기 원하는 값을 우리가 직접 지정해 준다.
본래 데이터 저장 함수이기에 데이터 저장 성공 후 응답 값이 다음과 같다고 가정해보자.
{
"result": "success",
"id": "testUser@test.com"
}
이런 결과와 저장된 id를 json으로 응답 받았었다면 mock함수가 반환하길 원하는 값으로 저 json을 지정해주자.
우리는 controller의 로직에 집중해야하니 데이터베이스는 "대충 이런이런 값을 반환한다고 치자"라고 하고 넘어가는 개념이다.
3. 작성해본 테스트 코드
모킹을 하는 방법은 Jest에서 제공하는 fn(), spyOn(), mock() 함수를 사용하면 된다. 이 중에서 난 fn()을 쓰려고 했지만 원인 모를 에러로 인해 spyOn 함수를 사용했다!
이 3가지 함수에 대한 설명은 이 블로그에서 친절하게 설명을 해두었다.
UserController에 있는 getPassword함수를 테스트해보자.
getPassword 함수는 다음과 같다.
export const getPassword = async (req: Request, res: Response, next: NextFunction) => {
try {
const id: string = req.query.id ? req.query.id : req['claims']['email'];
return res.status(200).json(await account.findPasswordById(id));
} catch (e) {
Logger.error('%o', e);
return next(e);
}
};
getPassword함수는 사용자 Id를 받아서 db에서 password를 조회하는 함수다.
이 함수의 기능은 두가지다.
- req로부터 사용자 id를 추출하고,
- findPasswordById함수에 사용자 id를 넘겨준다.
http 요청을 만들기 위해 httpMocks라는 모듈을 사용하였다.
작성한 테스트코드는 다음과 같다.
import * as model from '../models/student.model';
test('[DB 데이터 조회 테스트] 사용자 id를 통해 password 조회 테스트', async () => {
const req = httpMocks.createRequest({
method: 'GET',
url: '/api/test?id=user@test.com',
});
const res = httpMocks.createResponse();
const next = null;
const expectedResult = { password: 'mypass123' };
const mockFindPasswordById = jest.spyOn(model, 'findPasswordById');
mockFindPasswordById.mockResolvedValue(expectedResult);
await UserController.getPassword(req, res, next);
expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toStrictEqual(expectedResult);
});
import한 model은 데이터베이스에 접근하는 함수들을 모아놓은 모듈이다.
11번째 줄의 spyOn함수를 통해서 mock 함수를 생성한다.
const mockFindPasswordById = jest.spyOn(model, 'findPasswordById');
import된 model 모듈에 존재하는,
findPasswordById 함수를 spyOn을 통해서,
mockFindPasswordById 라는,
mock함수로 만들었다.
findPasswordById는 원래 데이터베이스에 접근하는 함수였지만 이제 아무런 기능이 없는 mock 함수가 되었다.
호출하면 undefined만 반환한다.
우리는 이 함수가 데이터베이스에서 password를 조회해 반환해주기를 원한다. 그렇기에 우리가 원하는 반환 값을 직접 달아준다.
mockFindPasswordById.mockResolvedValue(expectedResult);
이제 mockFindPasswordById는 쿡 찌르면 우리가 지정해준 응답값을 반환해준다.
이는 "데이터베이스가 정상적으로 동작했다고 치자"라고 설정한것이다.
데이터베이스 관련 함수는 async/await으로 처리되기에 Promise를 resolve하는 mockResolvedValue를 사용했다.
다른 여러 종류의 반환값에 대한 반환값 지정 함수는 다음 문서에서 확인하면 된다.
https://jestjs.io/docs/mock-function-api
이제 우리는 UserController.getPassword(req, res, next)로 우리가 만들었던 req를 보내준다.
UserController.getPassword(req, res, next)에는 findPasswordById함수가 있는데 우리는 지금 이걸 mock으로 만들어 주었으니 무조건 우리가 지정한 응답을 줄 것이다.
await UserController.getPassword(req, res, next);
expect(res.statusCode).toBe(200);
expect(res._getJSONData()).toStrictEqual(expectedResult);
UserController.getPassword에 잘못된 점이 없다면 테스트는 성공한다.
4. 결론
우리는 이제 무엇에 테스트를 성공한것인가?
UserController의 getPassword함수에서
- req로부터 사용자 id를 추출하고,
- findPasswordById함수에 사용자 id를 넘겨준다.
이 두가지 로직에 대한 검증이 성공한 것이다.
(매우매우 간단한 테스트다)
이는 작은 단위의 단위테스트이고 supertest를 활용한 통합테스트도 작성하였다. 오히려 통합테스트가 더 직관적이라 테스트코드를 처음 작성해보는 나에겐 더 쉬웠다.
라고 말하지만 사실 통합테스트에서는 db의 endpoint를 test일 경우에만 local db로 동적으로 바꿔주는 작업 때문에 많이 고생했다.ㅠㅠ
다음 포스팅에서 하소연 해보겠다.
참고:
'NodeJS' 카테고리의 다른 글
[NodeJS] Swagger 자동 생성 라이브러리 swagger-autogen (0) | 2021.11.12 |
---|---|
[NodeJS] 기본값 파라미터(default parameter value) (0) | 2021.10.29 |
[NodeJS] tsconfig.json에 대해 알아보자 (0) | 2021.09.25 |
[NodeJS] npm install option (0) | 2021.09.25 |
[NodeJS] Helmet을 사용한 Express 보안 (0) | 2021.09.20 |