본문 바로가기

개발

JEST로 API 테스트 하기 feat : Express, MongoDB

TL; DR (Too Long; Didn't Read)

JEST, supertest 설치

npm install --save-dev jest supertest

JEST 셋업

// package.json
{
  ...,
  "scripts": {
    ...,
    "test": "jest"
  },
  ...
}

mongodb-memory-server 설치(Optional)

npm install --save-dev mongodb-memory-server

개괄적인 테스트 방법과 주의사항

1. 파일명은 *.jest.js 또는 (타입스크립트 애용자라면) *.jest.ts

2. 자주 사용할것은 beforeAll, afterAll, beforeEach, AfterEach, describe, test, it

3. it는 test의 alias이다. it를 사용할때에는 보통 should와 같이 시작한다.

4. 상위 스코프에서 afterEach, beforeEach문을 사용하면 하위 스코프에도 적용이 된다. 

5. 테스트 시작 "npm run test"

 

예제 코드

const request = require('supertest')
const {MongoMemoryServer} = require('mongodb-memory-server');
const mongoose = require('mongoose');
// MongoDB ODM은 자기 마음대로~~
const app = require('../servers/server')
// app은 expressJS 인스턴스를 의미합니다.

let mongoServer;

beforeAll(async () => {
// 모든 테스트가 실행되기전에 이게 무조건 먼저 실행된다.
    mongoServer = await MongoMemoryServer.create();

    const mongooseOpts = {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true
    }

    await mongoose.connect(mongoServer.getUri(), mongooseOpts);
})

afterAll(async () => {
// 모든 테스트가 종료되고 이게 마지막으로 실행된다.
    if (mongoose.connection) {
        await mongoose.connection.dropDatabase();
        await mongoose.disconnect();
    }
    if (mongoServer) {
        await mongoServer.stop();
    }
})

describe("GET '/api/users' ", () => {
    beforeAll(() => {
        //describe 문마다 사용가능
        User.create("user1")
        User.create("user2")
        User.create("user3")
    })

    afterAll(async () => {
        // 모든 데이테베이스 기록 삭제
        const collections = mongoose.connection.collections;
        for (const key in collections) {
            const collection = collections[key];
            await collection.deleteMany({});
        }
    })

    // async await 방법
    it("should return 200 and all users", async () => {
        await request(app)
            .get('/api/users')  // URI
            .expect(200)        // status code
            .then(({body: {success, message, users}}) => {
                expect(success).toTruthy();
                expect(message).toEqual("successfully find users");
                expect(users).not.toBeUndefined();
            })
    })

    
    // DoneCallback을 활용한 방법
    it("should return 200 and all users", (done) => {
        request(app)
            .get('/api/users')  // URI
            .expect(200)        // status code
            .then(({body: {success, message, users}}) => {
                expect(success).toTruthy();
                expect(message).toEqual("successfully find users");
                expect(users).not.toBeUndefined();
                done()
            })
            .catch(err => done(err))
    })
})

Why Test?

개인 프로젝트를 진행중에 API를 설계를 마치고 빠르게 코드를 작성해 나갔다.
하지만, 테스트를 진행하지 않고 더이상 진행은 힘들었는데,
Jest를 도입전의 테스트는

 

  1. 서버를 구동하고
  2. Postman으로 API가 위치한 URL을 호출하고
  3. Mongo Atlas앱으로 데이터베이스를 확인하고,
  4. 반환값과 데이터베이스를 모두 확인해서
  5. 작성한 API가 성공적으로 수행되는지 확인을 하였다.

여기까진 꽃밭이였지만....

jsonwebtoken(jwt)을 도입하면서 일이 조금 꼬였다. 사용자가 로그인 프로세스를 통해서 jwt을 발급받으면, 각 요청시마다 헤더에 x-access-token="abcdefg~~~" 식의 작업을 손수 복사 붙여넣기 해야했다.

누가 이런걸 좋아해...

뭐 헤더는 그렇다 치더라도, 각 리소스에 요청을 하려면 MongoDB의 ObjectID를 기입하는 방식으로 편하게 작성을 했는데 이것도 자동화가 필요했다

이건 하면서도 답답했다. ㅠㅠ

따라서 나는

테스팅을 자동화해야겠다!

라는 마음을 먹고 시작을 했다. 

Why Jest?

Jest를 처음부터 사용하려 했던것은 아니였다. Mocha를 처음에 사용하려 공부하려다가 사용자 설문에 의한 사이트인 https://stateofjs.com에 접속하게 되었다. 

https://2020.stateofjs.com/en-US/technologies/testing/

웹개발을 한다는 사람이면 설문을 마다할 사람이 그렇게 많지 않을 테니, 이 기준이 정확하다면,  취직을 준비하는 내게도 큰 이점이 존재할것이라고 생각을 했다. 또한 Mocha보다 쉽다는데, 육안으로 봤을땐 Jest의 초기설정을 제외하면 똑같다.

Jest를 조금 찾아보니깐 페이스북에서 만들어서 똑같은 회사에서 나온 리액트와도 궁합이 잘맞아 현재 프로젝트의 스택인 프론트 리액트 & 백엔드 ExpressJS조합에도 사용할수 있겠다 싶어서 바로 설치부터 해보았다. 

 

npm install --save-dev jest

--save-dev 옵션에 대해 모르면? 여기

설치가 다 되었으면 npm run test를 통해 테스트를 해보자

일단 package.json에 테스트 도구를 추가한다.

 

// package.json
{
  ...,
  "scripts": {
    ...,
    "test": "jest"
  },
  ...
}

그후 npm run test

regex를 좀 하면 Jest가 뭘찾는지 알수 있을 것이다.

expressJS를 사용한다고 가정하고 supertest라는 HTTP 검증도구를 사용하자.

npm install --save-dev supertest

app.js를 이렇게 작성해보았다. '/'에 요청하면 response.body에 json을 넣어주는 심플한 코드이다.

process.env.NODE_ENV를 체크 해서 포트가 열리지 않게 처리한다. 왜냐하면 테스트 코드가 병렬적으로 동작하면 한포트에 express가 동시에 포트를 listen할수 있어 에러가 발생하는 것을 방지하기 위함이다. 

이제 API 테스트를 해보자!! 테스트의 확장자는 *.test.js로 맞추면된다. 이름은 마음대로!

supertest는 request로 import하고, request에 express 인스턴스를 적재후 .get(URL), .post(URL), .put(URL), .delete(URL)로 리소스를 요청할수 있다. 

test함수로 시작하고 첫번째 인자는 테스트에 대한 설명, 두번째는 테스트를 진행하는 함수로 이루어져있다. 

나는 app.js를 테스트하니깐 app.test.js로 해봤다. 이제 테스트를 해보면?

잠깐 설명!

.expect(200)은 supertest의 것이다. 상태코드가 200인지 확인하고 아니면 멈춘다.
.then(({body : {success, message}}) => {})는 response.body에 들어있는 success와 message를 찾아서 인자로 사용하는 것이다. 
expect(success).toBeTruthy() 는 Jest의 것이다. success가 True이거나 참을 표현하는 모든값이면 통과한다. false, 0이런값이면 실패한다.
expect(message).toEqual("~~") 는 Jest의 것이다. message와 오른쪽의 검사하는 객체의 모든값이 동일하면 통과한다.

테스트를 해보기 위해 npm run test를 해봤다.

엥 너무 빠르다. 틀린 케이스를 넣어볼까?

에러가 나오게 있지도 않은 POST 메소드를 호출해보았다. 엥 근데 성공? 

문제가 딱 나왔다. node의 비동기식 프로그래밍을 jest에도 적용해야 테스트가 수월할것 같다. 

 

done vs (async / await)

방법이 두개다!

done 방법은 조금 구려보이는 doneCallback을 사용하고... 

async/await은 미래를 준비할수 있지 않을까? 라는 생각이 든다.

그래도 두개를 다 써보는게 어딜가도 적응할수 있을 것이다.

 

done()

인자에 done()을 추가한다.

done()을 추가하였다.

done()이 호출되면 jest가 테스트가 끝난것을 확인한다.

done을 추가하니 수행시간이 14ms로 시간이 늘어나면서 테스트가 되는것같다

또한 실패에 대한 결과도 done콜백으로 넘길수 있어서 좋아 보인다.

 

async / await

함수를 async로 변경한다.

async/ await를 사용하면 좋은 점은 다른 비동기 함수에 await만 붙이면 된다는 점이다.

이제 개략적인 API테스트를 위한 준비가 되었다.

처음에는 MongoDB서버에 test데이터베이스를 따로 만들었었는데, 각 단위테스트간의 독립성이 보장이 되지 않았다. 

따라서 각 테스트 인스턴스 별로 독립성을 보장할수 있는 mongodb-memory-server를 설치해서 이 문제를 해결한다.

 

일단 설치

npm install --save-dev mongodb-memory-server

간단한 메소드로 제어할수 있게 파일을 작성한다.

const mongoose = require('mongoose');
const {MongoMemoryServer} = require('mongodb-memory-server');

let mongoServer;

module.exports.connect = async () => {
    mongoServer = await MongoMemoryServer.create();
    const uri = mongoServer.getUri();

    const mongooseOpts = {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useCreateIndex: true
    }

    await mongoose.connect(uri, mongooseOpts);
}

module.exports.closeDatabase = async () => {
    if (mongoose.connection) {
        await mongoose.connection.dropDatabase();
        await mongoose.disconnect();
    }
    if (mongoServer) {
        await mongoServer.stop();
    }
}

module.exports.clearDatabase = async () => {
    const collections = mongoose.connection.collections;
    for (const key in collections) {
        const collection = collections[key];
        await collection.deleteMany({});
    }
}

connect : 새로운 서버 인스턴스를 생성하고 mongoose에 접속시킨다. 

closeDatabase : mongoose에서 접속을 해제하고, 서버를 종료한다.

clearDatabase : 저장된 모든 collection을 제거한다.

 

자. 써보자

나는 이렇게 하는데.. 나만 이런가?...

여기서 it는 test와 같은 역할을 하는 alias이다. 어감이 it('should blah blah')여서 테스트를 표현하기가 좋다.

현재 나는 RESTful API를 꿈꾸면서 API를 작성해서 응답을 (상태코드(200, 201, 400, 403, 404, 409...)와 success:boolean, message:String)형식으로 꾸렸다. 

사람이 항상 완벽할수 없다고 생각해서 성공 사례보다 실패 사례, 그러니깐 Error Handling을 중요시 생각했다. 

 

코드를 잠깐보면 beforeAll과 afterAll, describe들이 보인다. 

 

beforeAll은 모든 테스트 전에 수행되고, afterAll은 모든 테스트가 종료된 후에 수행된다. 

추가로 beforeEach, afterEach도 있는데 각 테스트 마다 수행하는 것이다.

 

describe는 각 테스트들을 그룹으로 묶어준다. 테스트 그룹으로 묶인 것들은 그 안에서 afterAll과 beforeAll을 사용할 수 있다. 하지만 이상한점이 beforeEach와 afterAll은 상위그룹에서 선언하면 하위그룹의 모든 테스트에서 실행이 된다. 

 

추가적으로 code coverage도 볼수있다. 

일단 global로 설치

npm install jest --global
jest --coverage

터미널에서도 볼수 있고~
엄청나게 예쁜 coverage리포트도 프로젝트 루트에 생긴다

커버리지 올리는데 재미가 너무 들려서 실제코드보다 테스트코드를 작성하는데 시간이 더 걸리는 문제가 발생해 버렸다. ㅠㅠ

 

Jest로 테스트를 한다는 구상은 Spring Framework를 배울때 신박하다고 생각하면서 시작을 하였지만, 이젠 Jest가 너무 편하게 되버렸다. 

업데이트가 생기면 바로 올리겠다.

틀린점이 있거나 코드가 작성이 안된다면 댓글로 작성해주세요.

 

Github Action도 작성해서 Merge전 코드도 테스트중인데 궁금하면 들어와 보시라! Tutor2Tutee-Advanced

 

 

반응형

'개발' 카테고리의 다른 글

[MAC] 맥에서 Visual Studio Code로 C/C++ 개발환경 구축하기  (4) 2020.07.28