테스트 준비하기
⇒ 테스트에 필요한 패키지는 jest이다. 이 패키지는 페이스북에서 만든 오픈 소스로
테스팅에 필요한 툴들을 갖추고 있어 편리하다.
- 테스트 파일이 될 대상은 파일명에 index.spec.js, index.test.js 가 들어가야 한다.
- npm 개발자모드로 설치
$ npm i -D jest
- pakage.json에 실행 스크립트 수정
// package.json
{
...
"scripts" : {
"start" : "nodemon app",
"test" : "ject",
},
...
}
- test 실행하기
$ npm test
11.2 단위(유닛) 테스트 해보기
⇒ 테스트 틀을 잡고 describe로 테스트를 그룹화하여 관리하자.
- middlewares 단위 테스트
// middlewares/index.js
// 로그인 한 여부 체크
exports.isLoggedIn = (req, res, next) => {
if(req.isAuthenticated()) {
next();
} else {
res.status(403).send('로그인 필요');
}
};
// 로그인 안한 여부 체크
exports.isNotLoggedIn = (req, res, next) => {
if(!req.isAuthenticated()) {
next();
} else {
const message = encodeURIComponent('로그인한 상태입니다.');
res.redirect(`/?error=${message}`);
}
}
⇒ exprect 메서드
- toEqual : 앞의 인수와 맞는지 체크
- toBeCalledWith로 인수 체크
- toBeCalledTimes로 호출 회수 체
// middlewares/index.test.js
const { isLoggedIn, isNotLoggedIn} = require('./index');
// 단위(유닛) 테스트
// test('1 + 1은 2 입니다.', () => {
// // 코드 -> 예상결과
// expect(1 + 1).toEqual(2);
// });
// describe는 비슷한 그룹끼리 묶어줄 수 있다.
describe(('isLoggedIn'), () => {
test('로그인이 되어 있으면 isLoggedIn이 next를 호출해야 함.', () => {
// 모킹이라는 개념으로 가짜 매개변수들을 만들어준다.
const res = {
status : jest.fn(() => res),
send : jest.fn(),
};
const req = {
// callback함수를 선언해 true를 리턴할 수 있다.
isAuthenticated : jest.fn(() => true),
};
// const next = () => {}; <-- 이런 빈 함수는 toBeCalledTimes에서 인식이 되질 않는다.
// next는 jest가 추적하는 함수가 된다.
const next = jest.fn();
// isLoggedIn을 호출하여 next가 한번 호출되었는지 확인할 수 있다.
isLoggedIn(req, res, next);
expect(next).toBeCalledTimes(1);
});
test('로그인이 되어 있지 있으면 isLoggedIn이 에러를 응답해야 함.', () => {
// 모킹이라는 개념으로 가짜 매개변수들을 만들어준다.
const res = {
status : jest.fn(() => res),
send : jest.fn(),
};
const req = {
isAuthenticated : jest.fn(() => false),
};
isLoggedIn(req, res);
// expect는 여러건 넣을 수 있으며 전부다 통과해야 성공이다.
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);
const message = encodeURIComponent('로그인한 상태입니다.');
expect(res.redirect).toBeCalledWith(`/?error=${message}`);
});
test('로그인이 되어 있지 있으면 isNotLoggedIn이 next를 호출해야 함.', () => {
const req = { isAuthenticated : jest.fn(() => false) };
isNotLoggedIn(req, res, next);
expect(next).toBeCalledTimes(1);
});
})
- controller user 단위 테스트
// controllers/user.js
const User = require('../models/user');
exports.follow = async (req, res, next) => {
try {
const user = await User.findOne({where : {id : req.user.id}});
if(user) {
// addFollowing이라는 메서드명이 어떻게 생기는건지 sequelize 학습하기
await user.addFollowing(parseInt(req.params.id, 10));
res.send('success');
} else {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
};
// controllers/user.test.js
jest.mock('../models/user'); // User를 대체하기 위한 초기설정
const User = require('../models/user');
const { follow } = require('./user');
// 테스트는 if문이나 try~caych를 기점으로 만들어 주는 편이 좋다.
describe(('follow'), () => {
test('사용자를 찾아 팔로잉을 추가하고 suceess를 응답해야 함', async () => {
const res = {
status : jest.fn(() => res),
send : jest.fn(),
};
const req = {
user : {id : 1},
params : {id : 2},
};
const next = jest.fn();
// User.findOne을 대체처리
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 () => {
const res = {
status : jest.fn(() => res),
send : jest.fn(),
};
const req = {
user : {id : 1},
params : {id : 2},
}
const next = jest.fn();
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 res = {
status : jest.fn(() => res),
send : jest.fn(),
};
const req = {
user : {id : 1},
params : {id : 2},
};
const next = jest.fn();
const message = 'DB에러';
User.findOne.mockReturnValue(Promise.reject(message));
await follow(req, res, next);
expect(next).toBeCalledWith(message)
});
});
⇒ 컨트롤러와 서비스를 분리 후 서비스 단위 테스트 수행
- 테스트의 우선순위 : 서비스 -> 컨트롤러
// services/user.js
const User = require('../models/user');
// service는 req, res를 몰라도 된다.
exports.follow = async (userId, followingId) => {
const user = await User.findOne({where : {id : userId}});
if(user) {
await user.addFollowing(parseInt(followingId, 10));
return 'ok';
} else {
return 'no user';
}
}
// controllers/user.js
// 기존 비지니스 로직을 제외한 컨트롤러에 대한 처리
const User = require('../models/user');
const {follow } = require('../services/user');
exports.follow = async (req, res, next) => {
try {
const result = await follow(req.user.id, req.params.id);
if(result === 'ok') {
res.send('success');
} else if(result === 'no user') {
res.status(404).send('no user');
}
} catch (error) {
console.error(error);
next(error);
}
};
// controllers/user.test.js
// service를 분리한 controller의 req, res 테스트
jest.mock('../services/user');
const { follow } = require('./user');
const { follow: followService } = require('../services/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() => {
followService.mockReturnValue('ok');
await follow(req, res, next);
expect(res.send).toBeCalledWith('success');
});
test('사용자를 못 찾으면 res.status(404).send(no user)를 호출함', async() => {
followService.mockReturnValue('no user');
await follow(req, res, next);
expect(res.status).toBeCalledWith(404);
expect(res.send).toBeCalledWith('no user');
});
test('DB에서 에러가 발생하면 next(error)를 호출함', async() => {
const message = 'DB에러';
followService.mockReturnValue(Promise.reject(message));
await follow(req, res ,next);
expect(next).toBeCalledWith(message);
});
})
11.3 테스트 커버리지
1. 테스트 커버리지란?
⇒ 테스트를 진행한 전체 코드 중 테스트되고 있는 코드의 비율을 의미한다.
테스트를 진행한 곳의 require된 부분에 대해서도 테스트 커버리지를 확인한다.
$ jest -coverage
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 90.9 | 90 | 100 | 90.9 |
controller | 100 | 75 | 100 | 100 |
user.js | 100 | 75 | 100 | 100 | 8
middlewares | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
models | 66.66 | 100 | 100 | 66.66 |
user.js | 66.66 | 100 | 100 | 66.66 | 42-47
services | 85.71 | 100 | 100 | 85.71 |
user.js | 85.71 | 100 | 100 | 85.71 | 7
-------------|---------|----------|---------|---------|-------------------
Test Suites: 2 failed, 2 passed, 4 total
Tests: 2 failed, 10 passed, 12 total
Snapshots: 0 total
Time: 0.875 s, estimated 1 s
2. 테스트 커버리지 올리기
⇒ models/model.js의 커버리지를 올리기 위한 무의미한 테스트 수행
// models/user.test.js
const Sequelize = require('sequelize');
const User = require('./user');
const config = require('../config/config')['test'];
const sequelize = new Sequelize(
config.database, config.username, config.password, config,
)
// 테스트 커버리지를 올리기 위한 테스트이다.
describe('User 모델', () => {
test('static initiate 메서도 호출', () => {
// return이 없기 때문에 undefined가 나온다.
expect(User.initiate(sequelize)).toBe(undefined);
});
test('static associate 메서도 호출', () => {
// return이 없기 때문에 undefined가 나온다.
const db = {
User : {
hasMany : jest.fn(),
belongsToMany : jest.fn(),
},
Post: {},
}
User.associate(db);
// tobe와 toHaveBeenCalledWith은 동일하다.
expect(db.User.hasMany).toHaveBeenCalledWith(undefined);
expect(db.User.belongsToMany).toHaveBeenCalledTimes(2);
});
})
- 모델 테스트 추가 후 커버리지 재 확인
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 96.96 | 90 | 100 | 96.96 |
controller | 100 | 75 | 100 | 100 |
user.js | 100 | 75 | 100 | 100 | 8
middlewares | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
models | 100 | 100 | 100 | 100 |
user.js | 100 | 100 | 100 | 100 |
services | 85.71 | 100 | 100 | 85.71 |
user.js | 85.71 | 100 | 100 | 85.71 | 7
-------------|---------|----------|---------|---------|-------------------
11.4 통합 테스트
1. 통합 테스트 해보기
⇒ Supertest를 사용하여 통합테스트를 수행한다.
$ npm i -D supertest
⇒ 라우터 하나를 통째로 테스트를 수행한다.(여러개의 미들웨어, 모듈을 한 번에 테스트)
- 서버를 실행하지 않고 테스트 하기 위한 app.js의 listen을 server.js로 분리한다.(package.json 설정도 변경필요)
// server.js
const app = require('./app');
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
// package.json
{
"main": "server.js",
"scripts": {
"start" "nodemon server",
...
}
const app = require('../app');
const request = require('supertest');
const { sequelize } = require('../models');
// beforeAll : 모든 테스트가 수행되기 전에 실행
// 테스트가 수행되면 회원가입이 진행되므로 force : true속성을 통한 데이터베이스 초기화를 한다.
// 테스트 결과는 일관성이 있어야 한다.
beforeAll(async () => {
await sequelize.sync({force : true});
})
// beforeEach : 테스트가 수행되기 전 반복적으로 실행된다.
beforeEach(() => {});
// reuqest(app).post(주소)로 요청
// send로 data를 전송
describe('POST /login', () => {
test('로그인 안 했으면 가입', (done) => {
request(app).post('/auth/join')
.send({
email : 'ykji1003@hotmail.co.kr',
nick : '김재익',
password : 'cpfl1318',
})
.expect('Location','/')
.expect(302, done);
})
test('회원가입이 되어 있는데 또 가입하는 경우', (done) => {
request(app).post('/auth/join')
.send({
email : 'ykji1003@hotmail.co.kr',
nick : '김재익',
password : 'cpfl1318',
})
.expect('Location','/join?error=exist')
.expect(302, done);
});
test('로그인 수행', (done) => {
request(app).post('/auth/login')
.send({
email : 'ykji1003@hotmail.co.kr',
password : 'cpfl1318',
})
.expect('Location', '/')
// Promise가 아닐 경우 done을 넣어줘야 jset가 해당 테스트가 끝난지 인식할 수 있다.
.expect(302, done);
});
test('가입되지 않은 회원인 경우.', (done) => {
const message = encodeURIComponent('가입되지 않은 회원입니다.');
request(app).post('/auth/login')
.send({
email : 'ykji1002@hotmail.co.kr',
password : 'cpfl1318',
})
// 테스트 결과는 send 후 결과가 expect와 일치해야 정상적으로 통과된다.
.expect('Location', `/?loginError=${message}`)
.expect(302, done);
});
test('비밀번호가 틀린 회원의 경우', (done) => {
const message = encodeURIComponent('비밀번호가 일치하지 않습니다.');
request(app).post('/auth/login')
.send({
email : 'ykji1003@hotmail.co.kr',
password : 'cpfl13181',
})
.expect('Location', `/?loginError=${message}`)
.expect(302, done);
});
});
describe('POST /join', () => {
// request(app)를 함께 사용하여 로그인 된 상태를 유지한다.
const agent = request.agent(app);
beforeEach((done) => {
agent.post('/auth/login')
.send({
email : 'ykji1003@hotmail.co.kr',
password : 'cpfl1318',
// 실행이 끝남을 알리기 위함
}).end(done);
});
test('로그인이 되어 있으면 회원가입 진행이 안되어야 한다.', (done) => {
const message = encodeURIComponent('로그인한 상태입니다.');
agent.post('/auth/join')
.send({
email : 'ykji1003@hotmail.co.kr',
nick : '김재익',
password : 'cpfl1318',
})
.expect('Location',`/?error=${message}`)
.expect(302, done);
});
});
describe('POST /logout', () => {
test('로그인이 되어 있지 않으면 403', (done) => {
request(app)
.get('/auth/logout')
.expect(403, done);
});
// beforeEach가 테스트가 수행될 때 항상 실행되지만
// agent를 독립적으로 사용하기 때문에 다른 request(app) 에이전트에 영향이 없다.
const agent = request.agent(app);
beforeEach((done) => {
agent
.post('/auth/login')
.send({
email : 'ykji1003@hotmail.co.kr',
password : 'cpfl1318',
}).end(done);
});
test('로그아웃 수행', (done) => {
agent
.get('/auth/logout')
.expect('Location', '/')
.expect(302, done);
});
});
// afterAll : 모든 테스트가 수행 끝난 후 실행
afterAll(() => {});
// afterEach : 테스트가 수행 끝난 후 반복적으로 실행된다.
beforeEach(() => {});
11.5 부하테스트
1. 부하 테스트란?
⇒ 서버가 얼마만큼의 요청을 견딜 수 있는지 테스트한다.
- 서버가 몇 명의 동시 접속자를 수용할 수 있는지 예측하기가 매우 어렵다.
- 실제 서비스 중이 아니라 개발 중일 때는 더 어렵다.
- 코드에 문제가 없더라도 서버 하드웨어 때문에 서비스가 중단될 수 있다.(메모리 부족 문제 등)
- 부하 테스트를 통해 위 문제들을 미리 예측하여 예방할 수 있다.
$ npm i -D artillery
$ npm start
2. Artillery 사용하기
⇒ 새 콘솔에서 다음 명령어를 입력한다.
// 100명의 사용자가 50번씩 요청을 보낸다.
$ npx artillery quick --count 100 -n 50 http://localhost:8001
// count : 가상의 사용자 수
// -n 옵션은 횟수
⇒ 결과보고서
- 사용자 생성(vusers.created)
- 테스트 성공(cusers.completed)
- 요청 성공 횟수(http.codes.200)
- 초당 요청 처리 횟수(http.requrest_rate)
- 응답 지연 속도(http.respose_rate)
- Min : 최소, Max : 최대, median : 중간 값
- P95 : 하위 95%, P99 : 하위 99% (하위는 속도 순서를 의미)
- Median과 P95가 많이 차이나지 않는게 좋은 상태이다.
http.codes.200: ................................................................ 5000
http.downloaded_bytes: ......................................................... 25085000
http.request_rate: ............................................................. 1128/sec
http.requests: ................................................................. 5000
http.response_time:
min: ......................................................................... 3
max: ......................................................................... 90
mean: ........................................................................ 50.7
median: ...................................................................... 51.9
p95: ......................................................................... 71.5
p99: ......................................................................... 77.5
http.responses: ................................................................ 5000
vusers.completed: .............................................................. 100
vusers.created: ................................................................ 100
vusers.created_by_name.0: ...................................................... 100
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1915.4
max: ......................................................................... 2734.2
mean: ........................................................................ 2546.2
median: ...................................................................... 2618.1
p95: ......................................................................... 2725
p99: ......................................................................... 2725
3. 여러 페이지 요청 시나리오
⇒ loadtest.json에 사용자의 행동 흐름 작성 가능
- target : 요청 도메인
- Phases에서 duration : 몇 초 동안(30초)
- arrivalRate : 매초 몇 명(20명)
- flow : 사용자의 이동
- url은 이동한 url
- json은 서버로 전송한 데이터 값
{
"config" : {
"target" : "http://localhost:8001",
"http" : {
"timeout" : 30
},
"phases" : [{
"duration" : 30,
"arrivalRate" : 20
}]
},
"scenarios" : [
{
"flow" : [
{ "get" : { "url" : "/" }},
{ "post" : {
"url" : "/auth/login",
"json" : {
"email" : "ykj1003@hotmail.co.kr", "password" : "cpfl1318"
},
"followRedirect" : false
}
},
{
"get" : {
"url" : "/hashtag?hashtag=강아지"
}
}
]
}
]
}
4. 여러 페이지 요청 시나리오(실행)
$ npx artillery run loadtest.json
http.codes.200: ................................................................ 124
http.codes.302: ................................................................ 62
http.downloaded_bytes: ......................................................... 548390
http.request_rate: ............................................................. 82/sec
http.requests: ................................................................. 186
http.response_time:
min: ......................................................................... 0
max: ......................................................................... 4
mean: ........................................................................ 1.4
median: ...................................................................... 1
p95: ......................................................................... 2
p99: ......................................................................... 3
http.responses: ................................................................ 186
vusers.completed: .............................................................. 62
vusers.created: ................................................................ 62
mean: ........................................................................ 8
median: ...................................................................... 6.9
p95: ......................................................................... 12.6
p99: ......................................................................... 25.8
⇒ 요청 후반부가 될 수록 응답속도가 길어질 수 있다. 그럴 경우 서버 사양을 업그레이드하거나, 서버를 여러 개
두거나 코드를 개선하여 부하를 줄일 수 있다.
- 현재는 싱글코어만 사용하므로, 클러스터링 기법 도입을 시도해 볼 수 있다.
- arrivalRate를 줄이거나 늘려서 어느정도 수용이 가능한지 부하 체크를 하는 것이 좋다.
- 여러 번 테스트 해 평균치를 내라.
5. 테스트 범위
⇒ 다양한 종류의 테스트를 주기적으로 수행해 서비스를 안정적으로 유지하는게 좋다.
- 자신이 짠 코드는 최대한 많이 테스트하자.
- 테스트하기 어려운 패키지는 모킹
- 모킹해서 통과하더라도 실제 상황에서는 에러날 수 있음을 염두헤 두어야 한다.
- 시스템 테스트 : QA처럼 테스트 목록을 두고 체크해 나가면서 진행하는 테스트이다.
- 인수 테스트 : 알파 테스트/베타 테스트처럼 특정 사용자 집단이 실제로 테스트
ps. 좀더 세부적인 테스트를 위한 유료 테스트를 제공하는 소프트웨어
'BackEnd > Node' 카테고리의 다른 글
[노드교과서] 섹션 12. 13장 실시간 경매 시스템 만들기(서버센트이벤트, 스케줄링) (2) | 2024.01.31 |
---|---|
[노드교과서] 섹션 11. 12장 실시간 GIF 채팅방 만들기(웹소켓, Socket.IO) (0) | 2024.01.28 |
섹션 9. 10장 API 서버 만들기(JWT, CORS) (0) | 2024.01.17 |
[노드교과서] 섹션 9. 노드버드 SNS 만들기 (0) | 2024.01.12 |
[노드교과서] 섹션 7. MongoDB (0) | 2024.01.07 |