API 를 테스트하는 유닛테스트를 작성해보자.
REST API 를 작성하면서 사용하는 라이브러리, 테스트 코드에 사용하는 라이브러리를 추가한다.
npm i express joi
npm i -D chai-http
express
는 Http(또는 Https)서버를 실행할 수 있는 가장 인기있고, 가장 가벼운 nodejs 라이브러리 중 하나이다.
joi
는 object validation 라이브러리이다.
chai-http
는 Http 관련 기능을 chai 이용해서 assert 할 수 있도록 만들어주는 라이브러리이다.
두 개의 파일을 생성한다.
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);
첫번째로 모든 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();
});
});
});
});
첫번째로 작성 테스트 코드는 별 이상없이 성공한다.
잘못된 형식으로 전체 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();
});
});
});
});
// 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();
});
});
});
});
// 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();
});
});
});
});
// 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();
});
});
});
});
// 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();
});
});
});
})
// 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();
});
});
});
})
위의 모든 테스트를 실행하면 다음과 같이
모든 결과가 성공했음을 확인할 수 있다.