Unit test Nodejs project with mocha, chai, supertest and sinon
Bài đăng này đã không được cập nhật trong 3 năm
Hiện nay việc viết unit test là một phần không thể thiếu trong mỗi dự án. Điều đó cũng cần thiết khi chúng ta thực hiện deploy với hệ thống CI/CD. Hôm nay mình sẽ giới thiệu qua việc viết test api với cho Nodejs project với mocha
, chai
, supertest
và sinonjs
.
Getting started
Trước tiên cần chuẩn bị một project Nodejs với một số modules cần thiết sau:
- mocha: Javascript test framework (ngoài ra chúng ta có thể dùng
Jest
) - chai: BDD/TDD assertion library
- sinon: Standalone test spies, stubs and mocks for JavaScript
- supertest: HTTP assertion
Ok giờ chạy npm init
để tạo một project, mình init một project với file package.json
(đây là file đủ sau khi mình thêm scripts vào rồi) như sau:
{
"name": "unittest",
"version": "1.0.0",
"description": "nodejs unit test",
"main": "index.js",
"scripts": {
"compile": "./node_modules/.bin/babel src --out-dir dist --ignore '**/*.test.js'",
"test": "mocha src/test/*.test.js --compilers js:babel-core/register",
"coverage": "nyc --reporter=html npm test"
},
"author": "kominam",
"license": "MIT",
"dependencies": {
"babel-polyfill": "^6.26.0",
"body-parser": "^1.18.2",
"express": "^4.15.4",
"mongoose": "^4.11.12",
"nyc": "^11.2.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.0",
"chai": "^4.1.2",
"mocha": "^3.5.3",
"sinon": "^3.3.0",
"supertest": "^3.0.0"
}
}
Vì mình viết dưới dạng es6 nên có dùng thêm babel
để compile js file và nyc
để xem code coverage.
Mình làm việc với MongoDB nên dùng thằng mongoose
(Mongoose) để thao tác dễ dàng hơn. Đầu tiên tạo 1 model với schema có tên là Todo
như sau:
import mongoose, { Schema } from 'mongoose';
const todoSchema = new Schema({
content: String,
isComplete: {
type: Boolean,
default: false
}
});
export default mongoose.model('Todo', todoSchema);
Tiếp theo tạo controller với 2 methods index
và store
:
require('babel-polyfill');
import Todo from '../models/Todo';
let index = async function (req, res) {
try {
let todos = await Todo.find({});
res.status(200).json({
data: todos
})
} catch(err) {
res.status(500).json({
err
});
}
}
let store = async function (req,res) {
try {
let newTodo = await Todo.create({
content: req.body.content
});
res.status(200).json({
data: newTodo
})
} catch(err) {
res.status(500).json({
err
});
}
}
export { index, store };
Định nghĩa router với express.Router()
:
import express from 'express';
import { index, store } from '../controller/TodoController';
const router = express.Router();
router.get('/api/v1/todos', index);
router.post('/api/v1/todos', store);
export default router;
Tạo server server.js
như sau:
import express from 'express';
import chalk from 'chalk';
import mongoose from 'mongoose';
import bodyParser from 'body-parser';
import router from './routes/web';
const PORT = process.env.PORT || 8000;
const MONGODB_URI = 'mongodb://localhost:27017/todos-ex'
mongoose.connect(
MONGODB_URI, {
useMongoClient: true
}
);
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// router
app.use('/', router);
app.listen(8000, () => {
console.log('%s I am ready to serve\n',
chalk.green('✓'));
console.log('-> Press CTRL-C to stop\n');
})
export default app;
Unit test with sinon
Tiếp theo là viết unit test cho TodoController
sử dụng stub
của sinon
:
supertest
: HTTP assertions
Nói qua một chút về mock
, stub
và spy
:
spy
: hiểu đơn giản thì nó là 1 function ghi lại các tham số, giá trị trả về... liên quan đến lần gọi của function.stub
: cũng giống như 1 spy, nhưng nó sẽ thay đổi hoạt động của function bằng cách return lại giá trị mình muốn.mock
: là fake methods (như spy) cùng với pre-programmed behavior (giống như stub) và cảpre-programmed expectations
.
import { assert, expect } from 'chai';
import { stub } from 'sinon';
import request from 'supertest';
import server from '../server';
import Todo from '../models/Todo';
describe('TodoController Unit Test', () => {
it('should get todos', () => {
const findAllTodoStub = stub(Todo, 'find').yields(undefined, [
{
_id: '59c60a137c02831d3f45f771',
content: 'Nodejs Learning Curve ',
__v: 0,
isComplete: false
}
]);
findAllTodoStub.withArgs({});
request(server)
.get('/api/v1/todos')
.expect(200, {
data: [
{
_id: '59c60a137c02831d3f45f771',
content: 'Nodejs Learning Curve ',
__v: 0,
isComplete: false
}
]
});
findAllTodoStub.restore();
});
it('can not get all because something went wrong with our database' ,() => {
const findAllTodoStub = stub(Todo, 'find').yields(new Error('Request to DB timeout'), undefined);
findAllTodoStub.withArgs({});
request(server)
.get('/api/v1/todos')
.expect(500, {
err: 'Request to DB timeout'
});
findAllTodoStub.restore();
})
});
yields
: gần giống nhưcallsArg
. Nó sẽ invoke callback thông qua argument truyền vào. Nếu stub không được gọi với function của argument đó thìyields
sẽ trả về lỗi. Ở ví dụ này khi gọi methodTodo.find()
thành công nó sẽ trả về bản 1 bản ghiTodo
. Ở trường hợp thứ 2 mình giả định rằng kết nối tới database hết hạn nênyields
sẽ trả về lỗi.restore
: là một trong số utilities của sinon. Nó sẽ restore lại tất cả fake method đc cung cấp bởi 1 object truyền vào. ở ví dụ này nó sẽ restore lạifindAllTodoStub
trước khi request tới server.withArgs
: chỉ stub method cho những argument cụ thể nào đó. Ở ví dụ này mình không truyền vào argument nào cả.
Compile and run test
chạy script npm run compile
để compile source code và sau đó chạy npm test
để chạy test ta có kết quả như sau:
> unittest@1.0.0 test /home/do.van.nam/Desktop/Workspaces/nodejs-unitTest
> mocha src/test/*.test.js --compilers js:babel-core/register
✓ I am ready to serve
-> Press CTRL-C to stop
TodoController Unit Test
✓ should get todos
✓ can not get all because something went wrong with our database
2 passing (20ms)
Hoặc mọi người có thể xem coverage với nyc
(người yêu cũ ) bằng việc chạy scripts npm run coverage
. Nó sẽ generate ra 1 folder coverage
ở ngay thư mục gốc của project. Mọi người mở file index.html trong đó và kết quả sẽ như thế này:
Happy coding !
References
All rights reserved