Testing API end point in Nodejs project with mocha, chai, supertest and sinon

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 rất cần khi ta làm 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 vài modules rất cần sau:

mocha: Javascript test framework (thêm nữa 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"   },   "lập trình viênDependencies": {     "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 vận chuyển tận nhàe coverage.
Mình làm việc với MongoDB nên dùng thằng mongoose(Mongoose) để thao tác dẽ dàng. Đầ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.urlenvận chuyển tận nhàed({ 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 dùng stub của sinon:

supertest : HTTP assertions

Nói qua một chút về mock, stub và spy:

spy: hiểu dễ làm thì nó là 1 function ghi lại các tham số, giá trị trả về… quan hệ đến lần gọi của function. stub: cũng giống như 1 spy, nhưng nó sẽ đổi khác làm việc 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 method Todo.find() thành công nó sẽ trả về bản 1 bản ghi Todo. Ở hoàn cảnh thứ 2 mình giả định rằng liên kết tới database hết hạn nên yields 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 ứng bởi 1 object truyền vào. ở ví dụ này nó sẽ restore lại findAllTodoStub 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 vận chuyển tận nhàe và tiếp theo chạy npm test để chạy test ta có kết quả như sau:

> [email protected] 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ũ :D) 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:
Coverage

Happy vận chuyển tận nhàing !

References

Sinonjs
Supertest
Source vận chuyển tận nhàe

Nguồn viblo.asia