1) jest란?
jest : 페이스북에서 만든 오픈소스로, 테스팅에 필요한 툴들을 대부분 갖추고 있는 패키지
npm i -D jest
{
"name": "nodebird",
"version": "0.0.1",
"description": "익스프레스로 만드는 SNS 서비스",
"main": "app.js",
"scripts": {
"start": "nodemon app",
"test": "jest"
},
...
}
2) 테스트용 파일 생성
// routes/middlewares.test.js
test("1 + 1은 2입니다.", () => {
expect(1 + 1).toEqual(2);
});
test(테스트에 대한 설명, 함수(테스트 내용))
expect(실제 코드)
toEqual(예상되는 결과값)
3) 테스트 실행
npm test
1) isLoggedIn, isNotLoggedIn 함수 테스트
describe(그룹 설명, 함수(그룹에 대한 내용)) 함수 : 테스트를 그룹화 해주는 역할
const { describe } = require("../models/user");
const { isLoggedIn, isNotLoggedIn } = require("./middlewares");
describe("isLoggedIn", () => {
test("로그인 되어 있으면 isLoggedIn이 next를 호출해야 함", () => {
});
test("로그인 되어 있지 않으면 isLoggedIn이 에러를 응답해야 함", () => {
});
});
describe("isNotLoggedIn", () => {
test("로그인되어 있으면 isNotLoggedIn이 에러를 응답해야 함", () => {
});
test("로그인되어 있지 않으면 isNotLoggedIn이 next를 호출해야 함", () => {
});
});
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}`);
}
};
모킹 : 가짜 객체, 가짜 함수를 넣는 행위
jest.fn() : 모킹할 때 사용하는 메서드
jest.fn(() => 반환값) : 함수의 반환값을 지정하고 싶을 때
toBeCalledTimes(숫자) : 정확하게 몇 번 호출되었는지를 체크하는 메서드
toBeCalledWith(인수) : 특정 인수와 함께 호출되었는지를 체크하는 메서드
const { isLoggedIn, isNotLoggedIn } = require("./middlewares");
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("로그인 되어있지 않으면 isLoggedIn이 에러를 응답해야 함", () => {
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);
});
});
유닛 테스트 : 작은 단위의 함수나 모듈이 의도된 대로 정확히 작동하는지 테스트하는 것
2) 미들웨어 테스트
const express = require("express");
const { isLoggedIn } = require("./middlewares");
const User = require("../models/user");
const router = express.Router();
router.post("/:id/follow", isLoggedIn, async (req, res, next) => {
try {
const user = await User.findOne({ where: { id: req.user.id } });
if (user) {
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);
}
});
module.exports = router;
const User = require('../models/user');
exports.addFollowing = async (req, res, next) => {
try {
const user = await User.findOne({ where: { id: req.user.id } });
if (user) {
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);
}
};
const express = require('express');
const { isLoggedIn } = require('./middlewares');
const { addFollowing } = require('../controllers/user');
const router = express.Router();
router.post('/:id/follow', isLoggedIn, addFollowing);
module.exports = router;
jest.mock("../models/user");
const User = require("../models/user");
const { addFollowing } = require("../controllers/user");
describe("addFollowing", () => {
const req = {
user: { id: 1 },
params: { id: 2 },
};
const res = {
send: jest.fn(),
};
const next = jest.fn();
test("사용자를 찾아 팔로잉을 추가하고 success를 응답해야 함", async () => {
User.findOne.mockReturnValue(
Promise.resolve({
addFollowing(id) {
return Promise.resolve(true);
},
})
);
await addFollowing(req, res, next);
expect(res.send).toBeCalledWith("success");
});
test("사용자를 못 찾으면 res.status(404).send(no user)를 호출함", async () => {
User.findOne.mockReturnValue(null);
await addFollowing(req, res, next);
expect(res.status).toBeCalledWith(404);
expect(res.send).toBeCalledWith("no user");
});
test("DB에서 에러가 발생하면 next(error) 호출함", async () => {
const error = "테스트용 에러";
User.findOne.mockReturnValue(Promise.reject(error));
await addFollowing(req, res, next);
expect(next).toBeCalledWith(error);
});
});
1) 커버리지 기능이란?
커버리지 기능 : 전체 코드 중에서 테스트되고 있는 코드의 비율과 테스트되고 있지 않은 코드의 위치를 알려주는 jest의 기능
2) jest 설정 입력
jest --coverage : jest가 테스트 커버리지를 분석
{
"name": "nodebird",
"version": "0.0.1",
"description": "익스프레스로 만드는 SNS 서비스",
"main": "app.js",
"scripts": {
"start": "nodemon app",
"test": "jest",
"coverage": "jest --coverage"
},
3) 테스트 커버리지 분석
4) models/user.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 init 메서드 호출', () => {
expect(User.init(sequelize)).toBe(User);
});
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);
});
});
5) 테스트 수행 - 통과
6) 테스트 커버리지 분석 - 커버리지 증가
1) supertest 패키지 설치
supertest 용도 - auth.js의 라우터들 테스트
npm i -D supertest
2) app.js 파일에서 app 객체 모듈로 만들기 (supertest 사용하기 위함)
...
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV != "production" ? err : {};
res.status(err.status || 500);
res.render("error");
});
module.exports = app;
3) server.js에서 app 객체 불러와 listen
server.js 역할 - app의 포트 리스닝
const app = require("./app");
app.listen(app.get("port"), () => {
console.log(app.get("port"), "번 포트에서 대기중");
});
4) npm start 명령어 변경
{
"name": "nodebird",
"version": "0.0.1",
"description": "익스프레스로 만드는 SNS 서비스",
"main": "app.js",
"scripts": {
"start": "nodemon server",
"test": "jest",
...
}
5) config/config.json에서 test 속성 수정
{
"development": {
"username": "root",
"password": "비밀번호",
"database": "nodebird",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": "비밀번호",
"database": "nodebird_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
...
}
6) 콘솔에서 nodebird_test 데이터베이스 생성
npx sequelize db:create --env test
7) 테스트 코드 작성
beforeAll : 테스트 실행하기 전에 사용되는 함수
afterAll : 모든 테스트가 끝난 후 사용되는 함수
beforeEach : 각각의 테스트 수행 전에 사용되는 함수
afterEach : 각각의 테스트 수행 후에 사용되는 함수
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: 'zerohch0@gmail.com',
nick: 'zerocho',
password: 'nodejsbook',
})
.expect('Location', '/')
.expect(302, done);
});
});
describe('POST /login', () => {
const agent = request.agent(app);
beforeEach((done) => {
agent
.post('/auth/login')
.send({
email: 'zerohch0@gmail.com',
password: 'nodejsbook',
})
.end(done);
});
test('이미 로그인했으면 redirect /', (done) => {
const message = encodeURIComponent('로그인한 상태입니다.');
agent
.post('/auth/join')
.send({
email: 'zerohch0@gmail.com',
nick: 'zerocho',
password: 'nodejsbook',
})
.expect('Location', `/?error=${message}`)
.expect(302, done);
});
});
describe('POST /login', () => {
test('가입되지 않은 회원', (done) => {
const message = encodeURIComponent('가입되지 않은 회원입니다.');
request(app)
.post('/auth/login')
.send({
email: 'zerohch1@gmail.com',
password: 'nodejsbook',
})
.expect('Location', `/?loginError=${message}`)
.expect(302, done);
});
test('로그인 수행', (done) => {
request(app)
.post('/auth/login')
.send({
email: 'zerohch0@gmail.com',
password: 'nodejsbook',
})
.expect('Location', '/')
.expect(302, done);
});
test('비밀번호 틀림', (done) => {
const message = encodeURIComponent('비밀번호가 일치하지 않습니다.');
request(app)
.post('/auth/login')
.send({
email: 'zerohch0@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: 'zerohch0@gmail.com',
password: 'nodejsbook',
})
.end(done);
});
test('로그아웃 수행', (done) => {
agent
.get('/auth/logout')
.expect('Location', `/`)
.expect(302, done);
});
});
afterAll(async () => {
await sequelize.sync({ force: true });
});
7) 통합 테스트 결과
1) artillery 설치 및 서버 실행
npm i -D artillery
npm start
2) 새로운 콘솔 띄운 후 명령어 입력
npx artillery quick --count 100 -n 50 http://localhost:8001
[안될 경우 artillery 버전 변경]
npm i -D artillery@1.5.0-12 후에, 명령어 다시 실행
[명령어 옵션]
--count : 가상의 사용자 수
-n : 요청 횟수
3) 부하 테스트 결과
4) 시나리오 작성
{
"config":{
"target": "http://localhost:8001",
"phases": [
{
"duration": 60,
"arrivalRate": 30
}
]
},
"scenarios": [{
"flow": [{
"get": {
"url": "/"
}
}, {
"post": {
"url": "/auth/login",
"json": {
"email": "zerohch0@naver.com",
"password": "nodejsbook"
}
}
}, {
"get": {
"url": "/hashtag?hashtag=nodebird"
}
}]
}]
}
5) 시나리오 부하 테스트 실행
npx artillery run loadtest.json
6) 부하 테스트 결과
[Node.js] 12장(2) 미들웨어와 소켓 연결하기 (0) | 2022.01.10 |
---|---|
[Node.js] 12장 웹소켓으로 실시간 데이터 전송하기(1) (0) | 2022.01.06 |
[Node.js] 10장 웹 API 서버 만들기 (0) | 2021.12.27 |
[Node.js] 9장 익스프레스로 SNS 서비스 만들기 (0) | 2021.12.27 |
[Node.js] 8장 몽고디비 (0) | 2021.12.01 |