테스트 준비
NodeBird 서비스에 테스팅을 적용해보자. 실제 서비스를 개발 완료한 후, 개발자나 QA들은 자신이 만든 서비스가 제대로 동작하는지 테스트해본다. 이때 기능이 많다면 일일이 수작업으로 테스트하기에는 작업량이 너무 많다.
이런 경우 테스트를 자동화하여 프로그램이 프로그램을 테스트하도록 만들기도 한다.
하지만 아무리 테스트를 철저하게 해도 에러를 완전히 막는 것은 불가능하다.
NodeBird 프로젝트에 jest 패키지를 설치하자.
npm i -D jest
package.json을 다음과 같이 수정하자.
"scripts": {
"start": "nodemon app",
"test": "jest"
},
routes 폴더 안에 middlewares.test.js를 만들자. 테스트용 파일은 파일명과 확장자 사이에 test나 spec을 넣으면 된다.
그리고 npm test로 테스트 코드를 실행할 수 있다.
테스트 코드를 작성해보자.
test('1 + 1 = 2', () => {
expect(1 + 1).toEqual(2);
});
유닛 테스트
실제 NodeBird의 코드를 테스트해보자. middlewares.js에 있는 isLoggedIn과 isNotLoggedIn 함수를 테스트해보자.
const { describe } = require('../models/user');
const {isLoggedIn, isNotLoggedIn} = require('./middlewares');
describe('isLoggedIn', () => {
test('로그인되어 있으면 isLoggedIn이 next를 호출해야 함', () => {
});
test('로그인되어 있지 않으면 isLoggedIn이 error를 응답해야 함', () => {
});
});
describe('isNotLoggedIn', () => {
test('로그인되어 있으면 isNotLoggedIn이 error를 응답해야 함', () => {
});
test('로그인되어 있지 않으면 isNotLoggedIn이 next를 호출해야 함', () => {
});
});
NodeBird/routes/middlewares.test.js
실제 코드에서는 익스프레스가 req, res 객체와 next 함수를 인수로 넣었기에 사용할 수 있었지만, 테스트 환경에서는 어떻게 넣어야 할지 고민된다. req 객체에는 isAuthenticated 메서드가 존재하고 res 객체에도 status, send, redirect 메서드가 존재하는데, 코드가 성공적으로 실행되게 하려면 이것들을 모두 구현해야 한다.
이럴 때는 가짜 객체와 함수를 만들어 놓으면 된다. 이러한 행위를 모킹(mocking)이라고 한다.
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이 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);
});
});
NodeBird/routes/middlewares.test.js
res, res, next를 모킹했다. 함수를 모킹할 때는 jest.fn 메서드를 사용하면 된다. status 같은 경우에는 반환값이 있기 때문에 위와 같은 형태를 지닌다. expect를 이용하여 원하는 내용대로 실행되는지 체크하면 된다.
toBeCalledTimes(숫자)는 정확하게 몇 번 호출되었는지를 체크하는 메서드고, toBeCalledWith(인수)는 특정 인수와 함께 호출되었는지를 체크하는 메서드이다.
이렇게 작은 단위의 함수나 모듈이 의도된 대로 정확히 동작하는지 테스트하는 것을 유닛 테스트 또는 단위 테스트라고 부른다.
라우터와 긴밀하게 연결되어 있는 미들웨어도 테스트해보자. 이때 유닛 테스트를 위해 미들웨어를 분리해야 한다.
routes/user.js파일을 봐보자.
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(err){
console.error(err);
next(err);
}
});
module.exports = router;
NodeBird/routes/user.js
POST /:id/follow 라우터의 async 함수 부분은 따로 분리할 수 있다.
controllers폴더를 만들고 그 안에 user.js를 만들자. 라우터에서 응답을 보내는 미들웨어를 특별히 컨트롤러라고 부른다.
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(err){
console.error(err);
next(err);
}
};
NodeBird/controllers/user.js
router를 다음과 같이 수정하자.
const express = require('express');
const { isLoggedIn } = require('./middlewares');
const {addFollowing} = require('../controllers/user');
const User = require('../models/user');
const router = express.Router();
router.post('/:id/follow', isLoggedIn, addFollowing);
module.exports = router;
NodeBird/routes/user.js
이제 addFollowing 컨트롤러를 테스트해보자.
jest.mock('../models/user');
const User = require('../models/user');
const {addFollowing} = require('./user');
describe('addFollowing', () => {
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(Promise.resolve({
addFollowing(id){
return Promise.resolve(true);
}
}));
await addFollowing(req, res, next);
expect(res.send).toBeCalledWith('success');
});
test('사용자를 못 찾으면 res.status(404).send(nou 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(err) 호출', async () => {
const error = '테스트용 에러';
User.findOne.mockReturnValue(Promise.reject(error));
await addFollowing(req, res, next);
expect(next).toBeCalledWith(error);
});
});
NodeBird/controller/user.test.js
addFollowing 컨트롤러 안에는 User라는 모델이 들어 있다. 이 모델은 실제 데이터베이스와 연결되어 있으므로 테스트 환경에서는 사용할 수 없다. 따라서 User 모델도 모킹해야 한다. jest.mock 메서드에 모킹할 모듈의 경로를 넣고 불러온다. mockReturnValue라는 메서드는 가짜 반환값을 지정한다.