TDD 004: 유닛테스트 작성하기 - API

Raymond Yoo·2022년 1월 15일
0

TDD

목록 보기
4/6
post-custom-banner

API 를 테스트하는 유닛테스트를 작성해보자.

소스코드 링크


프로젝트 설정 (추가)

REST API 를 작성하면서 사용하는 라이브러리, 테스트 코드에 사용하는 라이브러리를 추가한다.

npm i express joi
npm i -D chai-http

express 는 Http(또는 Https)서버를 실행할 수 있는 가장 인기있고, 가장 가벼운 nodejs 라이브러리 중 하나이다.
joi 는 object validation 라이브러리이다.
chai-http 는 Http 관련 기능을 chai 이용해서 assert 할 수 있도록 만들어주는 라이브러리이다.

REST API 생성

두 개의 파일을 생성한다.
src/rest-api.js
src/utils/task-schema.js

// src/rest-api.js
const express = require("express");
const app = express();
const utils = require("./utils/task-schema.js");

app.use(express.json());

const tasks = [
  {
    id: 1,
    name: "Task 1",
    completed: false,
  },
  {
    id: 2,
    name: "Task 2",
    completed: false,
  },
  {
    id: 3,
    name: "Task 3",
    completed: false,
  },
];

// * GET
app.get("/api/tasks", (request, response) => {
  response.send(tasks);
});

// * GET (BY ID)
app.get("/api/tasks/:id", (request, response) => {
  const taskId = request.params.id;
  const task = tasks.find((task) => task.id === parseInt(taskId));
  if (!task)
    return response
      .status(404)
      .send("The task with the provided ID does not exist.");
  response.send(task);
});

// * POST
app.post("/api/tasks", (request, response) => {
  const { error } = utils.validateTask(request.body);

  if (error)
    return response
      .status(400)
      .send("The name should be at least 3 chars long!");

  const task = {
    id: tasks.length + 1,
    name: request.body.name,
    completed: request.body.completed,
  };

  tasks.push(task);
  response.status(201).send(task);
});

// * PUT
app.put("/api/tasks/:id", (request, response) => {
  const taskId = request.params.id;
  const task = tasks.find((task) => task.id === parseInt(taskId));
  if (!task)
    return response
      .status(404)
      .send("The task with the provided ID does not exist.");

  const { error } = utils.validateTask(request.body);

  if (error)
    return response
      .status(400)
      .send("The name should be at least 3 chars long!");

  task.name = request.body.name;
  task.completed = request.body.completed;

  response.send(task);
});

// * PATCH
app.patch("/api/tasks/:id", (request, response) => {
  const taskId = request.params.id;
  const task = tasks.find((task) => task.id === parseInt(taskId));
  if (!task)
    return response
      .status(404)
      .send("The task with the provided ID does not exist.");

  const { error } = utils.validateTask(request.body);

  if (error)
    return response
      .status(400)
      .send("The name should be at least 3 chars long!");

  task.name = request.body.name;

  if (request.body.completed) {
    task.completed = request.body.completed;
  }
  response.send(task);
});

// * DELETE
app.delete("/api/tasks/:id", (request, response) => {
  const taskId = request.params.id;
  const task = tasks.find((task) => task.id === parseInt(taskId));
  if (!task)
    return response
      .status(404)
      .send("The task with the provided ID does not exist.");

  const index = tasks.indexOf(task);
  tasks.splice(index, 1);
  response.send(task);
});

const port = process.env.PORT || 4000;
module.exports = app.listen(port, () =>
  console.log(`Listening on port ${port}...`)
);
// src/utils/task-schema.js
const Joi = require('joi');

const taskSchema = Joi.object({
    name: Joi.string().min(3).required(),
    completed: Joi.boolean()
});

module.exports = {
  validateTask: (task) => taskSchema.validate(task),
};

테스트 코드 작성 시작

// test/rest-api.js
const chai = require("chai");
const chaiHttp = require("chai-http");
const server = require("../src/rest-api");

const expect = chai.expect;

// Http 관련 기능을 chai 와 연동해서 사용하려면 반드시 필요한 코드이다.
chai.use(chaiHttp);

HTTP Method

HTTP Request Methods - MDN

  • GET
    서버에서 데이터 불러오기
    Read, Retrieve
  • POST
    서버에 새로운 데이터 생성하기
    Create, Save
  • PUT
    서버에 있는 데이터 수정하기, 수정하려는 단위 데이터 전체를 전송해야 한다
    Replace, Fully Update
  • PATCH
    서버에 있는 데이터 수정하기, 수정하려는 데이터의 일부분만 전송해도 된다
    서버는 클라이언트가 보내준 일부의 데이터만 변경하고 나머지는 이전 상태 그대로 유지한다
    Set, Partial Update, Merge Update
  • DELTE
    서버에 있는 데이터 삭제하기
    Remove, Erase

GET /api/tasks

첫번째로 모든 tasks 를 배열 형태로 불러오는
서비스 코드를 테스트해보자.

// test/rest-api.js
...
describe("Tasks API", function () {
  // * GET
  describe("GET /api/tasks", function () {
    it("should GET all the tasks", function (done) {
      // 읽는 그대로
      // server 로 요청을 보냄 -> get 요청 -> URL 은 /api/tasks
      // -> 응답 수신에 성공하면 response, 네트워크 요청 자체가 실패하면 error 로 처리
      chai
        .request(server)
        .get("/api/tasks")
        .end(function (error, response) {
          // 성공 시 statusCode 200 반환한다.
          expect(response).to.have.status(200);
          expect(response.body).to.be.a("array");
          expect(response.body.length).to.be.eq(3);
        
          // 테스트 케이스에서 promise 기반의 함수를 사용하면 
      	  // 마지막에 반드시 done 함수를 호출해야 한다.
          done();
        });
    });
  });
});

첫번째로 작성 테스트 코드는 별 이상없이 성공한다.

TDD 004 image 01

잘못된 형식으로 전체 tasks 불러오기 요청을 보내는 경우에는 실패하게 된다.
이에 대한 테스트 코드를 작성해보자.

// test/rest-api.js
...
describe("Tasks API", function () {
  // * GET
  describe("GET /api/tasks", function () {
    ...
    it("failed to GET all the tasks", function (done) {
      chai
        .request(server)
        // URL /api/task 는 라우터에 존재하지 않는다.
     	// 그러므로 statusCode 404 응답을 받는다.
        .get("/api/task")
        .end(function (error, response) {
          expect(response).to.have.status(404);
          done();
        });
    });
  });
});

GET /api/tasks/:id

// test/rest-api.js
...
describe("Tasks API", function () {
  ...
  
  // * GET by id
  describe("GET /api/tasks/:id", function () {
    it("should GET a task by id", function (done) {
      const taskId = 2;
      chai
        .request(server)
        .get(`/api/tasks/${taskId}`)
        .end(function (error, response) {
          expect(response).to.have.status(200);
          expect(response.body).to.be.a("object");
          expect(response.body).to.have.property("id");
          expect(response.body).to.have.property("name");
          expect(response.body).to.have.property("completed");
          expect(response.body).to.have.property("id").to.be.eq(2);
          done();
        });
    });

    it("faild to GET a task by id", function (done) {
      const taskId = 0;
      chai
        .request(server)
        .get(`/api/tasks/${taskId}`)
        .end(function (error, response) {
          expect(response).to.have.status(404);
          expect(response.text).to.be.eq(
            "The task with the provided ID does not exist."
          );
          done();
        });
    });
  });
});

POST /api/tasks

// test/rest-api.js
...
describe("Tasks API", function () {
  ...
  
  // * POST
  describe("POST /api/tasks", function () {
    it("should POST a new task", function (done) {
      const task = {
        name: "Task 4",
        completed: false,
      };
      chai
        .request(server)
        .post("/api/tasks")
        .send(task)
        .end(function (error, response) {
          expect(response).to.have.status(201);
          expect(response.body).to.be.a("object");
          expect(response.body).to.have.property("id");
          expect(response.body).to.have.property("name");
          expect(response.body).to.have.property("completed");
          expect(response.body).to.have.property("name").to.be.eq("Task 4");
          expect(response.body).to.have.property("completed").to.be.eq(false);
          done();
        });
    });

    it("failed to POST a new task without name property", function (done) {
      const task = {
        completed: false,
      };
      chai
        .request(server)
        .post("/api/tasks")
        .send(task)
        .end(function (error, response) {
          expect(response).to.have.status(400);
          expect(response.text).to.be.eq(
            "The name should be at least 3 chars long!"
          );
          done();
        });
    });
  });
});

PUT /api/tasks/:id

// test/rest-api.js
...
describe("Tasks API", function () {
  ...
  
  // * PUT
  describe("PUT /api/tasks/:id", function () {
    it("should PUT a task of specific id", function (done) {
      const taskId = 1;
      const task = {
        name: "Task 1 changed",
        completed: false,
      };
      chai
        .request(server)
        .put(`/api/tasks/${taskId}`)
        .send(task)
        .end(function (error, response) {
          expect(response).to.have.status(200);
          expect(response.body).to.be.a("object");
          expect(response.body).to.have.property("id");
          expect(response.body).to.have.property("name");
          expect(response.body).to.have.property("completed");
          expect(response.body).to.have.property("id").to.be.eq(1);
          expect(response.body)
            .to.have.property("name")
            .to.be.eq("Task 1 changed");
          expect(response.body).to.have.property("completed").to.be.eq(false);
          done();
        });
    });

    it(
      "failed to PUT a task of specific id " +
        "because the name property is less than 3 characters",
      function (done) {
        const taskId = 1;
        const task = {
          name: "Ta",
          completed: false,
        };
        chai
          .request(server)
          .put(`/api/tasks/${taskId}`)
          .send(task)
          .end(function (error, response) {
            expect(response).to.have.status(400);
            expect(response.text).to.be.eq(
              "The name should be at least 3 chars long!"
            );
            done();
          });
      }
    );

    it("failed to PUT a task of specific id because the id doesn't exist", function (done) {
      const taskId = 0;
      const task = {
        name: "Task 0",
        completed: false,
      };
      chai
        .request(server)
        .put(`/api/tasks/${taskId}`)
        .send(task)
        .end(function (error, response) {
          expect(response).to.have.status(404);
          expect(response.text).to.be.eq(
            "The task with the provided ID does not exist."
          );
          done();
        });
    });
  });
});

PATCH /api/tasks/:id

// test/rest-api.js
...
describe("Tasks API", function () {
  ...
  
  // * PATCH
  describe("PATCH /api/tasks/:id", function () {
    it("should PATCH a task of specific id", function (done) {
      const taskId = 1;
      const task = {
        name: "Task 1 patched successfully",
      };
      chai
        .request(server)
        .patch(`/api/tasks/${taskId}`)
        .send(task)
        .end(function (error, response) {
          expect(response).to.have.status(200);
          expect(response.body).to.be.a("object");
          expect(response.body).to.have.property("id");
          expect(response.body).to.have.property("name");
          expect(response.body).to.have.property("completed");
          expect(response.body).to.have.property("id").to.be.eq(1);
          expect(response.body)
            .to.have.property("name")
            .to.be.eq("Task 1 patched successfully");
          done();
        });
    });

    it(
      "failed to PATCH a task of specific id " +
        "because the name is less than 3 characters",
      function (done) {
        const taskId = 1;
        const task = {
          name: "Ta",
        };
        chai
          .request(server)
          .patch(`/api/tasks/${taskId}`)
          .send(task)
          .end(function (error, response) {
            expect(response).to.have.status(400);
            expect(response.text).to.be.eq(
              "The name should be at least 3 chars long!"
            );
            done();
          });
      }
    );

    it("failed to PATCH a task of specific id because the id doesn't exist", function (done) {
      const taskId = 0;
      const task = {
        name: "Task 1 patched successfully",
      };
      chai
        .request(server)
        .patch(`/api/tasks/${taskId}`)
        .send(task)
        .end(function (error, response) {
          expect(response).to.have.status(404);
          expect(response.text).to.be.eq(
            "The task with the provided ID does not exist."
          );
          done();
        });
    });
  });
})

DELETE /api/tasks/:id

// test/rest-api.js
...
describe("Tasks API", function () {
  ...
  
  // * DELETE
  describe("DELETE /api/tasks/:id", function () {
    it("should DELETE a task of specific id", function (done) {
      const taskId = 1;
      chai
        .request(server)
        .delete(`/api/tasks/${taskId}`)
        .end(function (error, response) {
          expect(response).to.have.status(200);
          done();
        });
    });

    it("failed DELETE a task of specific id because the id doesn't exist", function (done) {
      const taskId = 0;
      chai
        .request(server)
        .delete(`/api/tasks/${taskId}`)
        .end(function (error, response) {
          expect(response).to.have.status(404);
          expect(response.text).to.be.eq(
            "The task with the provided ID does not exist."
          );
          done();
        });
    });
  });
})

위의 모든 테스트를 실행하면 다음과 같이
모든 결과가 성공했음을 확인할 수 있다.

TDD 004 image 01

profile
세상에 도움이 되고, 동료에게 도움이 되고, 나에게 도움이 되는 코드를 만들고 싶습니다.
post-custom-banner

0개의 댓글