Jest
란 페이스북에서 개발한 오픈 소스 자바스크립트 테스트 프레임워크이다.- Jest를 사용하는 이유는 원래 mocha로 연습해보려 했으나, Jest에 필요한 함수들이 몇개 보여서 Jest로 바꿔서 연습했다.
- Jest를 사용하기 위해선 Jest를 설치해야 하며, 아래는 각각 글로벌로 설치하는 방법과 devDependencies에 로컬로써 설치하는 커맨드이다.
npm install -g jest
npm install --save-dev jest
- Jest를 글로벌로 설치하면 Jest CLI를 command line terminal 에서 사용할 수 있게 해준다.
- 아래 명령어를 통해 터미널에서 Jest를 사용한 테스팅을 시작할 수 있다.
jest
- 모카를 devDependencies에만 추가했다면, 아래 명령어를 통해 모카를 사용할 수 있다.
- 해당 프로젝트의 최상단이 cwd여야함.
./node_modules/mocha/bin/jest
- Jest는 실행시 자동으로 ./test/ 디렉토리에 존재하는 테스트 파일들을 찾는다.
- 따라서 아래 명령어를 통해
test
디렉토리를 만들어준다.
mkdir test
package.json
의script
내부test
를 아래와 같이 수정하면
/* package.json */
{
"scripts": {
"test": "jest"
}
}
- 아래와 같은 명령어로 테스트를 실행할 수 있다.
npm test
- 위 명령어를 입력하면 자동으로
package.json
에서test
항목으로 설정했던jest
명령어가 실행된다.
- Jest를 선택한 이유는
beforeAll()
메소드와afterAll()
메소드를 사용하기 위해서였다.- 이 함수들은 jest 명령어에 의해 테스트가 시작되면 처음에 한번, 마지막에 한번 메소드를 호출한다.
- 통합 테스트 하나에는 단위 테스트가 여러개 있으며 단위 테스트를 한번 실행할때마다 DB와 연결하고 끊는 행위는 비효율적이다.
- 따라서 보통 통합 테스트 시작 전에 연결한 후, 통합 테스트가 끝나면 마지막에 연결을 끊는다.
- 이때 사용되는 함수가
Jest
모듈의beforeAll()
,afterAll()
함수이다.
beforeAll(async () => {
/**
* Connecting MongoDB once the test has been started.
*/
await mongoose.connect(
process.env.MONGO_URI
)
.then(() => console.log('MongoDB conected...'))
.catch((err) => {
console.log(err);
});
});
describe('GET /test', () => {
it('response with html', (done) => {
request(app)
.get('/test/')
.expect('Content-Type','text/html; charset=UTF-8')
.expect(200)
.end((err, res) => {
if(err) {
done(err);
} else {
done();
}
});
});
});
afterAll(async () => {
/**
* Closing MongoDB.
*/
await mongoose.connection.close();
});
describe()
메소드를 기준으로 위의beforeAll()
메소드에서 DB와 연결해주며, 아래의afterAll()
메소드에서 DB와 연결을 해제해 준다.
describe()
는 테스트 그룹을 묶어주는 역할을 한다.
- 이 외에도 Jest에서 제공하는 다양한 함수들이 존재한다.
beforeEach()
,afterEach()
: 각 텍스트의 전 후마다 반복 실행..only()
:test.only()
처럼 해당 테스트만 임시로 실행하고 싶은 경우 사용..skip()
:test.skip()
처럼 해당 테스트는 임시로 스킵하고 싶은 경우 사용.
mocha
라고 불리는 브라우저나 노드에서 작동하는 오픈소스 자바스크립트 테스팅 프레임워크도 존재한다.
- mocha를 사용하기 위해서 모카 모듈 말고도 assertion 모듈도 필요하다.
- assertion은 특정 프로그램의 결과값과 기댓값이 일치하는지 확인해주는 기능을 보유하고 있다.
- 모카에서는 assertion 기능을 지원하는 어떠한 모듈을 사용해도 상관없다.
- 노드에서 지원하는 빌트인 모듈
assert
외에도Chai
,Expect.js
,Should.js
등 다양한 모듈이 있지만 이 게시글에서는Chai
를 사용한다.
Chai
는 테스트 시나리오 작성에 필요한 메소드를 제공하는 다양한 assertion 라이브러리중 하나이다.
- 여러개의 단위 테스트가 모여 API의 기능을 테스트 하는 통합테스트를 하기 위해선
supertest
라는 라이브러리가 필요하다.supertest
란 ExpressJS 통합 텍스트용 라이브러리로 내부에서 익스프레스 서버를 구동시켜 가상의 요청을 보내 결과를 검증할 수 있도록 도와주는 라이브러리이다.
- 실제로 서버를 돌리지 않고 앱 자체를 객체로 가져와 테스트 할 수 있다.
request()
메소드를 사용할 예정.
- 그렇다면 app.js를 객체로 가져오기 위해 해야할 일은 서버와 앱을 분리해야 한다.
- 원래 앱이 아래와 같은 형식으로 이루어져 있었다면,
const express = require('express');
const app = express();
const testRouter = require('./controllers/test/testRouter');
const port = 80;
app.listen(port, function() {
console.log('Listening on '+port);
});
app.use('/test', testRouter);
- CLI에서
node app
과 같은 형식의 명령어로 앱을 실행할 수 있었을 것이다.- 하지만 앞으로는 아래와 같이 app.js 와 server.js로 분리해 사용할 계획이다.
// app.js
const express = require('express');
const app = express();
const testRouter = require('./controllers/test/testRouter');
app.use('/test', testRouter);
module.exports = app;
// server.js
const app = require('./app');
const port = 80;
const server = app.listen(port, function() {
console.log('Listening on '+port);
});
- 기존의 app.js에서 서버 리스닝을 하지 않고 server.js를 추가해 server.js에서 리스닝 하는 이유는 API테스트를 할 때 실제 서버가 구동되어 버리는 경우를 방지하기 위함이다.
- 서버를 분리하지 않고 app객체를 가져오면 서버가 실행되버리기 때문에 분리후 실행하는 파일, 객체파일 두개로 나눴다.
"scripts": {
"test": "jest",
"start": "node server.js"
},
- 추가로
package.json
에서 서버 실행 명령어를node app.js
에서node server.js
로 변경해 준다.
supertest
의request()
는 가상의 서버를 실행하며 api를 요청할 수 있다.
const request = require('supertest');
const app = require('../app');
describe('GET /test', () => {
it('response with html', (done) => {
request(app)
.get('/test/')
.expect('Content-Type','text/html; charset=UTF-8')
.expect(200)
.end((err, res) => {
if(err) {
done(err);
} else {
done();
}
});
});
});
- 슈퍼 테스트 모듈을
request
변수에 담은 후 서버 객체를 가져와request()
에 넣어준다.- 그 후 이어서
get()
함수를 사용해 get요청을 할 수 있고 당연히post
,delete
,put
등 다양하게 사용할 수 있다.
- 헤더를 보내려면
set()
메소드를 이어서 사용해 주면 되며- 바디를 보내려면
send()
메소드를 사용하면 된다.- 마지막으로 이어서
expect()
함수로 응답을 검증한다.
- 위의 코드는 응답의 상태코드를 200으로 기대하며, Content-Type은 'text/html; charset=UTF-8'로 기대한다. 아닐시 에러가 나오면 실패.
- 아래는 테스트 시작 전 MongoDB와 연결 후 테스트를 실행한 후, DB와의 연결을 종료하는 테스트 코드이다.
- 하나의 통합 테스트는 3개의 단위 테스트로 이루어져 있으며, 토큰 발급, 토큰 확인, 토큰 삭제 순으로 진행된다.
- 로그인, 로그아웃.
🔽
server.spec.js
🔽
const request = require('supertest');
const path = require('path');
const mongoose = require('mongoose');
// calling enviroment variable from .env file
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
const app = require('../app');
beforeAll(async () => {
/**
* Connecting MongoDB once the test has been started.
*/
await mongoose.connect(
process.env.MONGO_URI
)
.then(() => console.log('MongoDB conected...'))
.catch((err) => {
console.log(err);
});
});
describe('CRUD API testing', () => {
describe('JWT token test', () => {
// In order to use cookies in more than one `it` methods.
var cookies;
it('Issuing a token', async function() {
var payload = {id:"one",pw:"one"};
payload = JSON.stringify(payload);
try {
var res = await request(app)
.post('/user/:id/:pw/')
.send(payload)
.expect(200);
} catch(e) {
console.log(e);
}
cookies = res.headers['set-cookie'][0];
});
it('Verifying a token', async () => {
const res = await request(app)
.get('/user/')
.set('Cookie', cookies)
.expect(200);
expect(res.text.includes(`<div class="login">`)).toEqual(true);
});
it('Removing token', async () => {
const res = await request(app)
.delete('/logout/');
expect(res.headers['set-cookie']).toEqual(undefined);
});
});
});
afterAll(async () => {
/**
* Closing MongoDB.
*/
await mongoose.connection.close();
});
// /user router
router.get('/', userMiddleWare.showMain);
router.post('/:id/:pw', userMiddleWare.signIn);
router.delete('/logout', userMiddleWare.signOut);
// Sing in.
exports.signIn = async (req, res) => {
var param = req.body;
try {
param = JSON.parse(Object.keys(param)[0]);
} catch(err) {
// console.log(err);
}
const {id, pw} = param;
const userConfirmed = await this.loginCheck(id, pw);
if(userConfirmed) {
const token = await this.issueToken(id);
return res
.cookie('user', token,{maxAge: 30 * 60 * 1000}) // 1000 is a sec
.end();
} else {
return res.status(200).send('Your password is not correct.');
}
}
- 아이디와 비밀번호를 one으로 미리 회원가입 해둔 후 테스트를 진행하였다.
- 물론 당연히 반복적으로 여러가지 아이디와 비밀번호를 자동으로 바꿔가며 테스트 할 수도 있다.
- id와 pw를 미리 만들어둔 one으로 설정한 후, POST메소드를 통해 요청하므로
send()
메소드에 정보를 담아 보낸다.- request의 리턴값을 res에 반환하며 해당 객체는 응답 객체가 되며, 서버에서 생성된 토큰은 헤더의 쿠키에 저장되어 있다.
// Main login page.
exports.showMain = (req, res) => {
const user = req.decoded;
if(user) {
return res.render(path.join(__dirname, '../../views/user/user'), {user:user});
} else {
return res.sendFile(path.join(__dirname, '../../views/user/user.html'));
}
}
- 토큰을 확인하는 부분은 토큰이 만약 유효하다면 정상적인 유저가 나왔으므로 ejs파일을 리턴할 때 유저에 관한 코드가 포함되 있으므로 그 부분이 있는지 확인하는 방법을 통해 테스트를 진행했다.
- 더 좋게 하고싶었는데 더 좋은 방법을 찾지는 못했다.
- 이 테스트는 사실 유저가 맞다고 가정하고 진행하는 테스트라 맞는 경우의 코드만 작성했음.
- 인증된 유저가 존재하지 않는다면 .html파일로 응답하게 되며, 해당 파일은
<div class="login">
태크를 포함하고 있지 않다.
// Sign out.
exports.signOut = (req, res) => {
return res.clearCookie('user').end();
}
- 쿠키를 지웠으므로, 해당 요청을 진행한 후 헤더에서 쿠키를 확인했을때 undefined가 나온다면 테스트에 통과하도록 작성했다.
- 위에서 설정을 다 해줬으므로, 아래의 명령어를 통해 실행할 수 있다.
npm test
~/Desktop/Desktop/CS/Practical/AWS/Project/Downloads mvc !2 ❯ npm test 18:53:29
> aws-node@1.0.0 test
> jest
console.log
MongoDB conected...
at log (test/server.spec.js:17:23)
PASS test/server.spec.js
CRUD API testing
GET /test
✓ response with html (32 ms)
JWT token test
✓ Issuing a token (115 ms)
✓ Verifying a token (14 ms)
✓ Removing token (4 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 2.315 s
Ran all test suites.
- 쓰고나서 보니 코드는 깃헙에서 보는게 더 편할거같다..