API TEST [ Node 백엔드 제작시 마주칠 것들 ]

개별 백엔드 API를 요청하고 결과값이 기대한 값과 같은지 확인하는 것이 E2E 테스트이다.

  1. jest, supertest 설치 및 실행
  2. 테스트 코드 기본 구성
  3. 테스트 코드 작성하기
  4. 응답 데이터의 일관성 유지하기

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 테스트 세팅은 정말 쉽다.

  1. supertest 객체를 import한다.
  2. 그 객체와 API 서버를 인자로 넘겨준다.
  3. 서버를 인자로 받은 supertest 객체에 api 요청을 진행한다.
  4. 그 요청의 응답이 우리가 기대한 값과 같은지 확인한다.
// 파일 구성
- 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테스트에 비해서는 어렵다.)


Reference