jest 로 api unit test 하기

Jinsukiya
6 min readMar 22, 2019

--

#1 이것은 유닛 테스트다. 통합 테스트가 아니란 말이다!!

unit : 단위

한마디로 유닛 테스트는 단위 테스트이다.

다시 말하면, 유닛 테스트 간 의존성이 있어서는 안된다. 하나의 유닛 테스트는 하나의 리소스를 테스트 할 필요가 있다.

더 정확히 말하면, 이전 유닛 테스트로 생성한 resource를 어련히 있겠거니 하고 테스트 하면 안된단 말이다.

각 유닛 테스트는 개별의 리소스를 생성하여 테스트 하여야 한다.

#2 jest에서 각 유닛 별 resource 독립적으로 가져가기

한 개의 유닛은 한 개의 describe로 내 나름대로 정의를 했다. 따라서 describe 별로 api를 테스트 하기 위한 resource를 만들어 둬야 한다. 또한 내가 정의한 rest API unit TEST는 다음과 같다.

GET : database 에 resource를 생성하고, api test로 잘 가져오는지 테스트 한다
POST : API test 로 요청하고, database에 resource가 잘 작성 되었는지 테스트 한다

내가 내린 정의에 맞추어 describe가 시작 할 때 리소스 생성이 필요하다.

리소스 생성 → api test / api test → 리소스 확인 의 순서로 동기로 이루어지면 참 좋겠지만 자바스크립트는 비동기 정확히 말하면 노드는 비동기로 작동한다. 따라서 비동기 함수를 시나리오에 따른 동기 함수화 시켜 줘야 하는데 주로 활용 하는 방식은 async await 이다.

근데… jest 의 describe는 async를 지원을 안한다 .. 정확히 말하면 describe에 async 를 넣게 되면 프로세스가 끝이 안 난다. 그래서 나는 이런 원칙을 세울 수 있었다.

  1. describe에서는 비동기 함수를 쓰지 않는다.
  2. describe가 시작 하기 전 필요한 resource를 생성 한다.

#3 jest는 life cycle이 있다.

앞서 말했다시피 노드는 비 동기다. 따라서 #2 에서 정의한 바가 우리가 원하는 순서대로 흘러가기 위해선 비 동기에 대한 처리를 해줘야 한다. 근데 describe 는 async를 지원하지 않기 때문에 그럼 코드를 이렇게 짜야 하나?

describe('UNIT TEST', () => {
models.User.create({ name: 'jinsuk', age: 27 }).then(user => {
test('should get user info', async done => {
return request()
.get(`/users/${user.id}`)
.then(res => {
expect(res.body.name).toEqual('jinsuk');
});
});
});
});
async await를 쓰는 javascript 개발자는 이런 코드를 짜지 않아!

모카, 차이 와 같은 테스트 프레임워크 들도 마찬가지지만 beforeAll, afterAll, beforeEach, afterEach 와 같은 life cycle이 존재한다.

describe('UNIT TEST', () => {
let user;
beforeAll(async done => {
user = await models.User.create({ name: 'jinsuk', age: 27 });
done();
});
test('should get user info', async done => {
return request()
.get(`/users/${user.id}`)
.then(res => {
expect(res.body.name).toEqual('jinsuk');
});
});
});

사실 리소스가 한 개라서 별 차이 없어 보이지만, 여러 개의 리소스를 비동기로 만들어야 한다면, 심지어 user가 속한 그룹의 테이블에 데이터를 먼저 만들고 id를 받아서 만들어야 한다면 ..? 상상하기도 싫은 코드가 만들어진다..

근데 코드를 보면 이상한 점이 있다. 과연 beforeAll의 done은 user가 생성 되는것을 기다려 줄까 …?

정답은 no다 user와 관련 이 없기 때문에 비동기로 흘러가고 done은 실행된다. 당연히 test가 돌기 시작한다.

여기서 많은 고민을 거친 결과 나온 만족스럽지는 않지만 이런 코드가 도출 된다.

describe('UNIT TEST', () => {
let user;
beforeAll(async done => {
user = await models.User.create({ name: 'jinsuk', age: 27 });
Promise.all([user]).then(() => {
done();
});
});
test('should get user info', async done => {
return request()
.get(`/users/${user.id}`)
.then(res => {
expect(res.body.name).toEqual('jinsuk');
});
});
});

어찌 되었건, test가 돌기전에 beforeAll에서 리소스를 완벽히 생성한 후 beforeAll을 완료 해주어야 한다.

#4 파일을 쪼개야 한다.

처음 시작 할 때는 사실 전체 beforeAll에서 sequelize db와 sync를 했다. 그런데 프로젝트가 진행되면 진행 될수록 당연히 테스트 케이스도 쌓여 나갔고, 500줄이 넘어가는 테스트 코드를 읽는데 조금 힘들다는 생각이 들었다. 당연히 파일을 분리 시키려는 시도를 하기 시작했다.

근데 원 파일의 beforeAll 에 있던 db sync는 당연히도 다른 파일을 기다려 주지 않았다. 그렇다고 해서 모든 파일 마다 DB를 열고 닫고를 할 수는 없었다.

https://jestjs.io/docs/en/configuration#globalsetup-string
{
... ,
"jest": {
"globalSetup": "./test/global/globalSetup.js",
"globalTeardown": "./test/global/globalTeardown.js"
}
}

package.json 에 다음과 같은 설정을 해주면 테스트 suite 가 돌기 전 setup 을 실행, 테스트가 종료 된 후 teardown을 실행하면서 종료 된다.

#1 ~ #4 를 요약 하면

유닛 테스트는 말 그대로 단위 테스트 이기 때문에 각 단위별 리소스를 독립적으로 가져가야 하고, 노드는 비동기로 이루어져 있지만, 테스트 프레임워크의 라이프 사이클을 적절히 이용하면, 우리가 원하는 순서대로 코드가 실행되게 할 수 있다.

아직 jest 의 기능을 1/10000 도 못쓰고 있는 것 같지만, 하루하루 1씩 늘려 가야겠다는 생각이 들었다.

--

--

Jinsukiya
Jinsukiya

Written by Jinsukiya

Softeware Engineer in codestates.

No responses yet