개별 백엔드 API를 요청하고 결과값이 기대한 값과 같은지 확인하는 것이 E2E 테스트이다.
- jest, supertest 설치 및 실행
- 테스트 코드 기본 구성
- 테스트 코드 작성하기
- 응답 데이터의 일관성 유지하기
1. jest, supertest 설치 및 실행
$ npm install --save-dev jest supertest
jest.config.js는 jest 설정을 모아두는 곳이다.
// jest.config.js
module.exports = {
name: "프로젝트 이름",
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
// __test__ 파일안의 .ts를 실행 혹은 .spec.ts .test.ts 파일이 해당한다.
};
설치와 설정을 해뒀으니 테스트를 실행할 스크립트를 적어두자. package.json
"scripts": {
"test": "jest",
}
실행할땐,
$ npm run test
2. 테스트 코드 기본 구성
Test 코드짜는 것은 서비스 코드보다 훨씬 구어체적이다. describe
, it
, expect
이 세가지가 핵심으로 진행된다.
describe
뜻대로 테스트를 설명,expect
뜻대로 테스트 예상값을 기대하다.it
는 줄임말이라서 뭐지 싶을텐데,individual test
의 줄임말이다. 개별 API마다describe
로 묶고 한 API의 테스트마다it
를 붙인다.
// posts.spec.ts
describe('GET /api/v1/posts/my', () => { // describe 로 하나의 API에 대한 테스트를 묶는다.
it('[success] 내 글 목록 가져오기', async () => {
~~ 테스트 코드 ~~
})
it('[fail] 로그인이 안되어서 실패', async () => {
~~ 테스트 코드 ~~
})
// 이처럼 하나의 API에 대한 여러 경우의 수들을 it로 하나하나 처리한다.
})
3. 테스트 코드 작성하기
supertest와 jest를 이용하는 API 테스트 세팅은 정말 쉽다.
- supertest 객체를 import한다.
- 그 객체와 API 서버를 인자로 넘겨준다.
- 서버를 인자로 받은 supertest 객체에 api 요청을 진행한다.
- 그 요청의 응답이 우리가 기대한 값과 같은지 확인한다.
// 파일 구성
- src
- test
- post.spec.ts
- controller
- service
- main.ts
1. supertest import하기
import request from "supertest";
2. supertest 객체에 서버 인자로 넣기.
import request from "supertest";
import server from "..main.ts";
request(server); // 인자로 서버를 넣기.
3. 서버를 인자로 넣은 supertest 객체에 API 요청하기
import request from 'supertest';
import server from '..main.ts'
describe("GET /api/v1/posts/my", () => {
it("[성공] 내 글 목록 가져오기 성공", async () => {
const response = await request(server)
.get("/api/v1/posts/my") // request 객체에 요청
4. 요청의 응답 값이 기대한 값과 같은지 확인하기.
import request from 'supertest';
import server from '..main.ts'
describe("GET /api/v1/posts/my", () => {
it("[성공] 내 글 목록 가져오기 성공", async () => {
const response = await request(server)
.get("/api/v1/posts/my")
expect(response.status).toEqual(200); // 응답값과 기대갑 비교.
추가적으로 자주사용하는 jest 메서드 종류.
.set()
헤더를 세팅할때 쓰인다..send()
API 바디를 넣을 때 이용한다..attach()
파일을 전달할때 이용한다..field()
파일을 입력할때는.send()
를 함께 이용하지 못하고..field()
를 이용해야한다.
다시 한번 아래 예시 코드를 쭉 읽어보자.
실제 테스트 예시
GET 방식 API 예시.
// src/test/post.spec.ts
import request from "supertest";
import server from "..main.ts";
describe("post 테스트", () => {
const access_token = "access_token=Berear abcdesdfegewfdsfk~skdhfkle";
describe("GET /api/v1/posts/my", () => {
it("[성공] 내 글 목록 가져오기 성공", async () => {
const response = await request(server)
.get("/api/v1/posts/my") // api 주소를 메서드의 인자로 넣는다.
.set("Cookie", [access_token]); // 서버에서 로그인 정보로 이용하게 될 jwt토큰을 전달한다.
expect(response.status).toEqual(200);
});
});
});
POST 방식 예시 (with File, without File)
// src/test/post.spec.ts
import request from "supertest";
import server from "..main.ts";
describe("post 테스트", () => {
const access_token = "access_token=Berear abcdesdfegewfdsfk~skdhfkle";
describe("GET /api/v1/posts/my", () => {
it("[성공] 내 글 작성 성공", async () => {
const response = await request(server)
.post("/api/v1/posts") // api 주소를 메서드의 인자로 넣는다.
.set("Cookie", [access_token]) // 서버에서 로그인 정보로 이용하게 될 jwt토큰을 전달한다.
.send({
// api 요청의 body를 send의 인자로 넣어준다.
title: "jackson",
content: "he is bascketball player",
});
expect(response.status).toEqual(200);
});
it("[성공] 내 글 작성 성공 with File", async () => {
const response = await request(server)
.post("/api/v1/posts") // api 주소를 메서드의 인자로 넣는다.
.set("Cookie", [access_token]) // 서버에서 로그인 정보로 이용하게 될 jwt토큰을 전달한다.
.field({
// api 요청의 body를 send의 인자로 넣어준다.
title: "jackson",
content: "he is bascketball player",
})
.attach("image1", `파일경로`); // 파일 업로드시에는 .atttach() 를 이용한다.
expect(response.status).toEqual(200);
});
it("[실패] 글 작성 실패", async () => {
const response = await request(server)
.post("/api/v1/posts") // api 주소를 메서드의 인자로 넣는다.
.set("Cookie", [access_token]) // 서버에서 로그인 정보로 이용하게 될 jwt토큰을 전달한다.
.field({
// api 요청의 body를 send의 인자로 넣어준다.
title: "jackson",
})
.attach("image1", `파일경로`); // 파일 업로드시에는 .atttach() 를 이용한다.
expect(response.status).toEqual(400);
expect(response.body.message).toEqual("content is empty");
});
});
});
4. 응답 데이터의 일관성 유지하기.
API를 만들 때 엄청 중요한 것중 하나가 API간의 인풋과 아웃풋 형식이 통일되어야한다는 점이다. 그래야만 실제 코드, 테스트 코드를 작성할 때 훨씬 편하고 시간적으로 절약된다.(백엔드입장) 또한 프론트 입장에서도 같은 형식으로 입력되고 응답하여야 API와 일할 때 명확하고 안정적으로 일할 수 있게 된다. TDD의 좋은점 중 하나가 이것이다. 미리 통일된 아웃풋 형식으로 테스트 코드를 작성해둬버리니 서비스 코드 개발 도중에 응답 코드가 중구난방이 되는 것을 막아준다.
만약 모든 API 성공, 실패 응답을 아래 형식대로 정했다면,
// 성공 응답
{
"pagination": {
hasBefore: false
hasNest: true,
currentPage: 1,
totalPage: 15
},
"data": {
users: [{
name: "kim",
age: 25
},{
name: "kim",
age: 25
},{
name: "kim",
age: 25
}]
}
}
// 실패 응답
{
"message": "Unauthorized"
}
모든 테스트에 .toHaveProperty()
테스트를 진행하여 응답 형식을 모두 체크해줄수 있게 된다.
describe('GET myUsers', () => {
it("[Success]", async () => {
// TEST
const response = await request(host)
.get(`/api/v1/users/my`)
.set('Cookie', [access_token])
expect(response.status).toEqual(200) // 성공 응답 코드 확인
expect(response.body).toHaveProperty("pagination") // 성공 데이터 형식 확인
expect(response.body).toHaveProperty("data") // 성공 데이터 형식 확인
})
it("[Fail]", () => {
const response = await request(host)
.get(`/api/v1/users/my`)
.set('Cookie', [wrong_token])
expect(response.status).toEqual(401); // 실패 응답 코드 확인
expect(response.body.message).toHaveProperty("message") // 실패 데이터 형식 확인.
})
유닛테스트가 아니라면, API 테스트는 정말 쉽다. 인풋 넣고 아웃풋확인하면 끝이니말이다. 알고리즘문제와 비슷하다. (유닛테스트는 중간 과정에서부터 테스트하고 여러가지 의존성들이 얽혀있어 api테스트에 비해서는 어렵다.)