진취적 삶
11 노드 서비스 테스트하기 본문
테스트 기법 중 유닛테스트 ,통합 테스트 ,부하 테스트 ,테스트 커버리지 체크를 해보자 .
11.1 테스트 준비하기
jest 패키지 설치
npm i -D jset
pacakge.json
"scripts": {
"start": "nodemon app",
"test": "jest"
},
npm test 테스트 코드 실행 가능
파일명에 test나 spec 이 들어간 파일들을 모두 찾아 실행한다.
// 첫번째 인수는 테스트 설명 , 두번째 인수는 테스트 내용
test(`1+1은 2 입니다`, () => {
expect(1 - 1).toEqual(2);
});
// 첫번째 인수는 테스트 설명 , 두번째 인수는 테스트 내용
test(`1+1은 2 입니다`, () => {
expect(1 + 1).toEqual(3);
});
FAIL middlewares/index.test.js
× 1+1은 2 입니다 (2 ms)
● 1+1은 2 입니다
expect(received).toEqual(expected) // deep equality
Expected: 3
Received: 2
1 | // 첫번째 인수는 테스트 설명 , 두번째 인수는 테스트 내용
2 | test(`1+1은 2 입니다`, () => {
> 3 | expect(1 + 1).toEqual(3);
| ^
4 | });
5 |
11.2 유닛 테스트
const { describe } = require("../../../10/nodebird-api/models/user");
const { isLoggedIn, isNotLoggedIn } = require("./");
describe("isLoggedIn", () => {
test("로그인 되어 있으면 isLoggedIn 이 next를 호출해야함 ", () => {});
test("로그인 되어 있지 않으면 isLoggedIn 이 어를 응답해야함", () => {});
});
describe("isNotLoggedIn", () => {
test("로그인되어 있으면 isNotLoggedIn이 에러를 응답해야합", () => {});
test("로그인되어 있지 않으면 isNotLoggedIn이 next를 호출해야함", () => {});
});
describe 는 테스트를 그룹화 해주는 역할
테스트 할때는 과감하게 가짜 객체와 함수를 만들어 넣으면 된다.
테스트 코드의 객체가 실제 익스프레스 객체가 아니어도 된다.
가짜 객체, 가짜 함수를 넣는 행위를 모킹이라고 한다.
const { isLoggedIn, isNotLoggedIn } = require("./");
describe("isLoggedIn", () => {
const res = {
status: jest.fn(() => res),
send: jest.fn(),
};
const next = jest.fn();
test("로그인되어 있으면 isLoggedIn 이 next를 호출해야함", () => {
const req = {
isAuthenticated: jest.fn(() => true),
};
isLoggedIn(req, res, next);
expect(next).toBeCalledTimes(1);
});
test("로그인 되어 있지 않으면 isLoggedInd이 error를 호출해야함", () => {
const req = {
isAuthenticated: jest.fn(() => false),
};
isLoggedIn(req, res, next);
expect(res.status).toBeCalledWith(403);
expect(res.send).toBeCalledWith("로그인 필요");
});
});
describe("isNotLoggedIn", () => {
const res = {
redirect: jest.fn(),
};
const next = jest.fn();
test("로그인되어 있으면 isNotLoggedIn이 에러를 응답해야합니다", () => {
const req = {
isAuthenticated: jest.fn(() => true),
};
isNotLoggedIn(req, res, next);
const message = encodeURIComponent("로그인한 상태");
expect(res.redirect).toBeCalledWith(`/?error=${message}`);
});
test("로그인되어 있지 않으면 isNotLoggedIn이 next를 호출해야함", () => {
const req = {
isAuthenticated: jest.fn(() => false),
};
isNotLoggedIn(req, res, next);
expect(next).toHaveBeenCalledTimes(1);
});
});
a함수를 모킹할때 는 jest.fn 메서드를 사용한다. 함수의 반환 값 저장은 jest.fn(()⇒반환값)을 사용
toBeCalledTimes (숫자)는 정확하게 몇 번 호출되었는지를 체크하는 메서드
toBeCalledWith (인수) 는 특정 인수와 함께 호출되었는지 체크하는 메서드
작은 단위의 함수나 모듈이 의도된 대로 정확히 작동하는지 테스트 하는것을 유닛 테스트
jest.mock("../models/user");
const { follow } = require("./user");
const User = require("../models/user");
describe("follow", () => {
const req = {
user: { id: 1 },
params: { id: 2 },
};
const res = {
status: jest.fn(() => res),
send: jest.fn(),
};
const next = jest.fn();
test("사용자를 찾아 팔로잉을 추가하고 success를 응답해야함", async () => {
User.findOne.mockReturnValue({
addFollowing(id) {
return Promise.resolve(true);
},
});
await follow(req, res, next);
expect(res.send).toBeCalledWith("success");
});
test("사용자를 못 찾으면 res.status(404).send(no user)를 호출함 ", async () => {
User.findOne.mockReturnValue(null);
await follow(req, res, next);
expect(res.status).toBeCalledWith(404);
expect(res.send).toBeCalledWith("no user");
});
test("DB에서 에러가 발생하면 next(error)를 호출함", async () => {
const message = "DB에러";
User.findOne.mockReturnValue(Promise.reject(message));
await follow(req, res, next);
expect(next).toBeCalledWith(message);
});
});
a 모킹할 모듈의 경로를 인수로 넣고 그 모듈을 불러온다.
11.3 테스트 커버리지
전체 코드 중에서 테스트 되고 있는 코드의 비율과 테스트 되고 있지 않은 코드의 위치를 알려주는 기능을 커버리지 기능이라 한다.
package.json 에 추가
"scripts": {
"start": "nodemon app",
"test": "jest",
"coverage": "jest --coverage"
},
npm run covergae
models / user test
const Sequelize = require("sequelize");
const User = require("./user");
const config = require("../config/config.json")["test"];
const sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
describe("User 모델", () => {
test("static initiate 메서드 호출", () => {
expect(User.initiate(sequelize)).toBe(undefined);
});
test("static associate 메서드 호출", () => {
const db = {
User: {
hasMany: jest.fn(),
belongsToMany: jest.fn(),
},
Post: {},
};
User.associate(db);
expect(db.User.hasMany).toHaveBeenCalledWith(db.Post);
expect(db.User.belongsToMany).toHaveBeenCalledTimes(2);
});
});
PASS models/user.test.js
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
controllers | 100 | 100 | 100 | 100 |
user.js | 100 | 100 | 100 | 100 |
middlewares | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
models | 100 | 100 | 100 | 100 |
user.js | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 3 passed, 3 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 1.283 s, estimated 2 s
커버리지 상승
11.4 통합 테스트
하나의 라우터를 통째로 테스트
실제 서비스 중인 데이터 베이스에 테스트용 데이터가 들어가면 안 되므로 , 테스트용 데이터 베이스를 따로 만드는것이 좋다.
const request = require("supertest");
const { sequelize } = require("../models");
const app = require("../app");
beforeAll(async () => {
await sequelize.sync();
});
describe("POST/login", () => {
test("로그인 수행 ", (done) => {
request(app)
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "password" })
.expect("Location", "/")
.expect(302, done);
});
});
beforeAll : 모든 테스트를 실행하기 전에 수행해야 할 코드를 넣는 공간이다.
sequelize.sync(): 데이터베이스에 테이블 생성
const request = require("supertest");
const { sequelize } = require("../models");
const app = require("../app");
beforeAll(async () => {
await sequelize.sync();
});
describe("Post/join", () => {
test("로그인 안 했으면 가입", (done) => {
request(app)
.post("/auth/join")
.send({
email: "hp980724@gmail.com",
nick: "suha",
password: "nodejs",
})
.expect("Location", "/")
.expect(302, done);
});
});
describe("POST/login", () => {
const agent = request.agent(app);
beforeEach((done) => {
agent
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "nodejs" })
.end(done);
});
test("이미 로그인 했으면 redirect /", (done) => {
const message = encodeURIComponent("로그인한 상태입니다");
agent
.post("/auth/join")
.send({ email: "hp980724@gmail.com", nick: "suha", password: "nodejs" })
.expect("Location", `/?error=${message}`)
.expect(302, done);
});
});
agent 만들어서 하나 이상의 요청에서 재사용 가능
beforeEach 는 각각의 테스트 실행에 앞서 먼저 실행되는 부분이다.
테스트 후 데이터 베이스에 데이터가 남아있으면 다음 테스트에 영향을 미칠수 있다
테스트 종료시 데이터를 정리하는 코드를 추가해야한다.
const request = require("supertest");
const { sequelize } = require("../models");
const app = require("../app");
beforeAll(async () => {
await sequelize.sync();
});
describe("POST/join", () => {
test("로그인 안 했으면 가입", (done) => {
request(app)
.post("/auth/join")
.send({
email: "hp980724@gmail.com",
nick: "suha",
password: "nodejs",
})
.expect("Location", "/")
.expect(302, done);
});
});
describe("POST/join", () => {
const agent = request.agent(app);
beforeEach((done) => {
agent
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "nodejs" })
.end(done);
});
test("이미 로그인 했으면 redirect /", (done) => {
const message = encodeURIComponent("로그인한 상태");
agent
.post("/auth/join")
.send({ email: "hp980724@gmail.com", nick: "suha", password: "nodejs" })
.expect("Location", `/?error=${message}`)
.expect(302, done);
});
});
//데이터 정리 테이블 다시 생성
afterAll(async () => {
await sequelize.sync({ force: true });
});
총 라우터 테스트
const request = require("supertest");
const { sequelize } = require("../models");
const app = require("../app");
beforeAll(async () => {
await sequelize.sync();
});
describe("POST/join", () => {
test("로그인 안 했으면 가입", (done) => {
request(app)
.post("/auth/join")
.send({
email: "hp980724@gmail.com",
nick: "suha",
password: "nodejs",
})
.expect("Location", "/")
.expect(302, done);
});
});
describe("POST/join", () => {
const agent = request.agent(app);
beforeEach((done) => {
agent
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "nodejs" })
.end(done);
});
test("이미 로그인 했으면 redirect /", (done) => {
const message = encodeURIComponent("로그인한 상태");
agent
.post("/auth/join")
.send({ email: "hp980724@gmail.com", nick: "suha", password: "nodejs" })
.expect("Location", `/?error=${message}`)
.expect(302, done);
});
});
describe("POST/login", () => {
test("가입되지 않은 회원", (done) => {
const message = encodeURIComponent("가입되지 않은 회원");
request(app)
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "nodejs" })
.expect("Location", `?loginError=${message}`)
.expect(302, done);
});
test("로그인 수행", (done) => {
request(app)
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "nodejs" })
.expect("Location", "/")
.expect(302, done);
});
test("비밀번호 틀림", (done) => {
const message = encodeURIComponent("비밀번호가 틀림");
request(app)
.post("/auth/login")
.send({ email: "hp980724@gmail.com", password: "wrong" })
.expect("Location", `?loginError=${message}`)
.expect(302, done);
});
});
describe("GET/logout", () => {
test("로그인되어 있지 않으면 403", (done) => {
request(app).get("/auth/logout").expect(403, done);
});
const agent = request.agent(app);
beforeEach((done) => {
agent
.post("/auth/login")
.send({
email: "hp980724@gmail.com",
password: "nodejs",
})
.end(done);
});
test("로그아웃 수행 ", (done) => {
agent.get("/auth/logout").expect("Location", "/").expect(302, done);
});
});
afterAll(async () => {
await sequelize.sync({ force: true });
});
11.5 부하 테스트
부하테스트 : 서버가 얼마만큼의 요청을 견딜수 있는지 테스트 하는 방법
내 서버의 몇 명의 동시 접속자나 일일 사용자를 수용할수 있는지 예측하는 일은 어렵다.
하드웨어의 제약 때문에 OOM (out of memory) 발생 가능
npx artillery quick --count 100 -n 50 <http://localhost:8001>
—count : 가상의 사용자
-n : 요청 횟수
100명이 50번 요청 5000번의 요청이 서버로 전달
All VUs finished. Total time: 11 seconds
--------------------------------
Summary report @ 18:55:42(+0900)
--------------------------------
http.codes.200: ................................................................ 5000
http.request_rate: ............................................................. 296/sec
http.requests: ................................................................. 5000
http.response_time:
min: ......................................................................... 12
max: ......................................................................... 441
median: ...................................................................... 159.2
p95: ......................................................................... 237.5
p99: ......................................................................... 320.6
http.responses: ................................................................ 5000
vusers.completed: .............................................................. 100
vusers.created: ................................................................ 100
vusers.created_by_name.0: ...................................................... 100
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 7585.1
max: ......................................................................... 8387.7
median: ...................................................................... 8186.6
p95: ......................................................................... 8352
p99: ......................................................................... 8352
http.response_time p95는 하위 95 %의 값 p99는 하위 99%의 값이다.
median과 p95의 차이가 크지 않은것이 좋다 .
실제 서비스에 부하테스트를 할 경우 서버가 중지되는 문제가 생길수도있다.
실제 서버와 같은 사양의 서버를 만든 후 , 그 서버에 부하 테스트를 진행하는것이 좋다.
{
"config": {
"target": "http:localhost:8001",
"http": {
"timeout": 30
},
"phase": [
{
"duration": 30,
"arrivalRate": 20
}
]
},
"scenarios": [
{
"flow": [
{
"get": {
"url": "/"
}
},
{
"post": {
"url": "/auth/login",
"json": {
"email": "hp980724@gmail.com",
"password": "nodejs"
},
"followRedirect": false
}
},
{
"get": {
"url": "/hashtag?hashtag=nodebird"
}
}
]
}
]
}
x타켓 현재 서버 잡고 phase에서 30초동안 사용자 20명
timeout 30초
가상 사용자들이 어떤 동작을 할지 시나리오에 적기
npx artillery run loadtest.json
datadog 나 newrelic 같은 상용 서비스를 적용해보면 좋다.
'개발 도서 > Node.js 교과서' 카테고리의 다른 글
13 실시간 경매 시스템 만들기 (0) | 2023.07.14 |
---|---|
12 웹 소켓으로 실시간 데이터 전송하기 (0) | 2023.07.14 |
10 웹 API 서버 만들기 (0) | 2023.07.14 |
09 익스프레스로 SNS 서비스 만들기 (0) | 2023.07.14 |
08 몽고디비 (0) | 2023.07.14 |