- vừa được xem lúc

Unit test cho Nodejs RESTful API với Mocha và Chai

0 0 24

Người đăng: hoangdv

Theo Viblo Asia

Giới thiệu

Chúng ta có thể tìm thấy nhiều ví dụ khởi tạo một RESTful API bằng Nodejs. Các bước thường thông thường sẽ là : Định nghĩa các packages sẽ dùng, khởi chạy một server với Express(Framework phổ biến và có nhiều hỗ trợ), định nghĩa các model, khai báo các router sử dụng ExpressRouter, và cuối cúng là Test API. Trong đó việc thực hiện test các Api là công việc mất nhiều thời gian, nhất là khi chúng ta thay đổi một model, sẽ có nhiều Api phải test lại. Việc viết unit test cho Api trở nên cực kỳ cần thiết, nhất là khi chúng ta tích hợp việc deploy với các hệ thống CI/CD. Trong bài này mình sẽ hướng dẫn một cách viết unit test RESTful API cho project viết bằng Nodejs sử dụng Mocha và Chai.

Mocha & Chai

Mocha: Là một javascript framework cho NodeJs cho phép thực hiện testing bất đồng bộ. Có thể nói đây là thư viện mà tôi thích nhất dùng để thực hiện viết test cho các dự án viết bằng Nodejs. Mocha có rất nhiều tính năng tuyệt vời, có thể tóm tắt những thứ mà tôi thích nhất của thư viện này :

  • Hỗ trợ bất đồng bộ đơn giản, bao gồm cả Promise.
  • Hỗ trợ nhiều hooks before, after, before each, after each (Rất tiện lợi cho bạn thiết lập và "làm sạch" môi trường test).
  • Có rất nhiều thư viện hỗ trợ việc xác định giá trị cần test (assertion). Chai là một thư viện tôi sử dụng trong bài viết này Chai: Assertion library. Trong bài viết này chúng ta phải test những Api có các phương thức GET, POST..., và phải kiểm tra đối tượng json mà Api trả về, đó là lý do ta phải dùng thêm Chai. Chai cung cấp nhiều tùy chọn Assertion cho việc thực hiện kiểm tra đối tượng: "should", "expect", "assert" Trong bài viết này chúng ta thêm addon "Chai HTTP" để thực hiện các HTTP requests và trả về giá trị của Api.

Chuẩn bị

  • Nodejs: Chúng ta cần có môi trường lập trình Nodej và hiểu biết cơ bản đủ có thể xấy dựng một RESTfull Api bằng nodejs
  • POSTMAN: Cho việc tạo http request tới Api
  • Cú pháp ES6: Việc này yêu cầu phiên bản của Nodejs phải từ 6.x.x trở lên

Chúng ta xây dựng một RESTful API đơn giản: Petstore

Cài đặt Project

Cấu trúc file và thư mục

Chuẩn bị thư mục dự án

$ mkdir petstore
$ cd petstore
$ npm init -y

Cấu trúc dự án

-- controllers 
---- models
------ pet.js
---- routes
------ pet.js
-- test
---- pet.js
package.json
server.json

package.json

{ "name": "petstore", "version": "1.0.0", "description": "A petstore API", "main": "server.js", "author": "hoangdv", "license": "ISC", "dependencies": { "body-parser": "^1.15.1", "express": "^4.13.4", "morgan": "^1.7.0" }, "devDependencies": { "chai": "^3.5.0", "chai-http": "^2.0.1", "mocha": "^2.4.5" }, "scripts": { "start": "node server.js", "test": "mocha --timeout 10000" }
}

Cài đặt các thư viện được định nghĩa trong file package.json

$ npm install

Server

server.js

let express = require('express');
let app = express();
let morgan = require('morgan');
let bodyParser = require('body-parser');
let port = process.env.PORT || 8080;
let pet = require('./routes/pet'); //don't show the log when it is test
if(process.env.NODE_ENV !== 'test') { //use morgan to log at command line app.use(morgan('combined')); //'combined' outputs the Apache style LOGs
} //parse application/json and look for raw text
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.text());
app.use(bodyParser.json({ type: 'application/json'})); app.get("/", (req, res) => res.json({message: "Welcome to our Petstore!"})); app.route("/pets") .get(pet.getPets) .post(pet.postPet);
app.route("/pets/:id") .get(pet.getPet) .delete(pet.deletePet) .put(pet.updatePet); app.listen(port);
console.log("Listening on port " + port); module.exports = app; // for testing

Model and Routes

Để thực hiện ví dụ cho bài viết, mình tạo mock một model pet ./model/pet.js

let ListData = [ {id: 1, name: 'Kitty01', status: 'available'}, {id: 2, name: 'Kitty02', status: 'available'}, {id: 3, name: 'Kitty03', status: 'available'}, {id: 4, name: 'Kitty04', status: 'available'}, {id: 5, name: 'Kitty05', status: 'available'}, {id: 6, name: 'Kitty06', status: 'available'}, {id: 7, name: 'Kitty07', status: 'available'}, {id: 8, name: 'Kitty08', status: 'available'}, {id: 9, name: 'Kitty09', status: 'available'}
];
module.exports.find = (callback) => { callback(null, ListData);
};
module.exports.findById = (id, callback) => { callback(null, ListData.find(item => item.id == id)); // typeof id === "string"
};
module.exports.save = (pet, callback) => { let {name, status} = pet; if (!name && !status) { callback("Pet is invalid"); return; } pet = { id: Date.now(), name, status }; ListData.push(pet); callback(null, pet);
};
module.exports.delete = (id, callback) => { let roweffected = ListData.length; ListData = ListData.filter(item => item.id != id); roweffected = roweffected - ListData.length; callback(null, {roweffected})
};
module.exports.update = (id, pet, callback) => { let oldPet = ListData.find(item => item.id == id); if (!oldPet) { callback("Pet not found!"); return; } let index = ListData.indexOf(oldPet); Object.assign(oldPet, pet); ListData.fill(oldPet, index, ++index); callback(null, oldPet);
};

TIếp theo là route cho server ./routes/pet.js


let Pet = require("../model/pet"); /* * GET /pets route to retrieve all the pets. */
let getPets = (req, res) => { Pet.find((err, pets) => { if (err) { res.send(err); // :D return; } res.send(pets); });
}; /* * POST /pets to save a new pet. */
let postPet = (req, res) => { let pet = req.body; Pet.save(pet, (err, newPet) => { if(err) { res.send(err); return; } res.send({ message: "Pet successfully added!", pet: newPet }); });
}; /* * GET /pets/:id route to retrieve a pet given its id. */
let getPet = (req, res) => { Pet.findById(req.params.id, (err, pet) => { if(err) { res.send(err); return; } res.send({ pet }); })
}; /* * DELETE /pets/:id to delete a pet given its id. */
let deletePet = (req, res) => { Pet.delete(req.params.id, (err, result) => { res.json({ message: "Pet successfully deleted!", result }); })
}; /* * PUT /pets/:id to update a pet given its id */
let updatePet = (req, res) => { Pet.update(req.params.id, req.body, (err, pet) => { if(err) { res.send(err); return; } res.send({ message: "Pet updated!", pet }); })
}; //export all the functions
module.exports = { getPets, postPet, getPet, deletePet, updatePet
};

Test

Native test

Chúng ta sử dụng POSTMAN để test các routes của server đã hoạt động như mong muốn chưa. Khởi chạy server:

$ npm start

GET /pets

POST /pets

GET /pets/:id

PUT /pets/:id

DELETE /pets/:id

Chúng ta may mắn mọi thứ hoạt động tốt, nó đã chạy mà không có lỗi nào. Nhưng điều này thì thật khó để đảm bảo cho một project thực tế, với lượng api lớn hơn rất nhiều và nghiệp vụ phức tạp, chúng ta sẽ mất rất nhiều thời gian để thực hiện hết các test với POSTMAN, chúng ta cần một các tiếp cận khác nhanh nhẹn hơn.

Unit test với Mocha và Chai

Tạo một file trong thư mục ./test với tên pet.js

//During the test the env variable is set to test
process.env.NODE_ENV = 'test'; //Require the dev-dependencies
let chai = require('chai');
let chaiHttp = require('chai-http');
let server = require('../server');
let should = chai.should(); chai.use(chaiHttp);
//Our parent block
describe('Pets', () => { beforeEach((done) => { //Before each test we empty the database in your case done(); }); /* * Test the /GET route */ describe('/GET pets', () => { it('it should GET all the pets', (done) => { chai.request(server) .get('/pets') .end((err, res) => { res.should.have.status(200); res.body.should.be.a('array'); res.body.length.should.be.eql(9); // fixme :) done(); }); }); });
});
  1. Ghi đè biến môi trường NODE_ENV=test, phục vụ cho việc những project chúng ta cấu hình môi trường test và prod khác nhau (database, api key...)
  2. Chúng ta định nghĩa should bằng cách khởi chạy chai.should();, để thực hiện ghi đè thuộc tính của Object cho việc thực hiện test. Mã nguồn thư viện:
...
Object.defineProperty(Object.prototype, 'should', { set: shouldSetter , get: shouldGetter , configurable: true });
...
  1. describe định nghĩ một block các test case cho cùng một loại.
  2. beforeEach là một hook được khởi chạy trước khi thực hiện các test được định nghĩa. Hook này giúp khởi tạo môi trường test dễ dàng(clear database, run init settup...)

Test /GET route

Test được định nghĩa trong block it should GET all the pets Kết quả mong muốn của API này sẽ là:

  1. http status là 200
  2. body trả về là một array
  3. độ dài của array9

Cú pháp kiểm tra khá gần với ngôn ngữ tự nhiên!

Run test

$ npm run test

Chúng ta có kết quả:

Test /POST route

describe('/POST pets', () => { it('it should POST a pet', (done) => { let pet = { name: "Bug", status: "detected" }; chai.request(server) .post('/pets') .send(pet) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Pet successfully added!'); res.body.pet.should.have.property('id'); res.body.pet.should.have.property('name').eql(pet.name); res.body.pet.should.have.property('status').eql(pet.status); done(); }); }); it('it should not POST a book without status field', (done) => { let pet = { name: "Bug" }; chai.request(server) .post('/pets') .send(pet) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql("Pet is invalid!"); done(); }); }); });

Test /GET/:id Route

 describe('/GET/:id pets', () => { it('it should GET a pet by the given id', (done) => { // TODO add a model to db then get that *id* to take this test let id = 1; chai.request(server) .get('/pets/' + id) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('pet'); res.body.pet.should.have.property('id').eql(id); res.body.pet.should.have.property('name'); res.body.pet.should.have.property('status'); done(); }); }); });

Test the /PUT/:id Route

describe('/PUT/:id pets', () => { it('it should UPDATE a pet given the id', (done) => { // TODO add a model to db then get that id to take this test let id = 1; chai.request(server) .put('/pets/' + id) .send({ name: "Bug", status: "fixed" }) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('pet'); res.body.pet.should.have.property('name').eql("Bug"); res.body.pet.should.have.property('status').eql("fixed"); done(); }); }); });

Test the /DELETE/:id Route

describe('/DELETE/:id pets', () => { it('it should DELETE a pet given the id', (done) => { // TODO add a model to db then get that id to take this test let id = 1; chai.request(server) .delete('/pets/' + id) .end((err, res) => { res.should.have.status(200); res.body.should.be.a('object'); res.body.should.have.property('message').eql('Pet successfully deleted!'); res.body.should.have.property('result'); res.body.result.should.have.property('roweffected').eql(1); done(); }); }); });

Hoàn thành viết file test, chúng ta thực hiện test

$ npm test

Kết quả:

Như vậy chúng ta đã hoàn thành việc viết unit test RESTfull API server viết bằng Nodejs. Với những trường ngôn ngữ server side khác chúng ta cũng có thể dùng Mocha và Chai để thực hiện test RESTfull API

chai.request('http://localhost:8080') .get('/')

Tổng kết

  • Các bước tạo ra một Server RESTfull API bằng Nodejs
  • Thực hiện triểm tra đơn giản các API bằng POSTMAN
  • Thực hiện viết unit test nhằm thực hiện tự động test Bài viết với hy vọng tạo ra một thói quen tốt khi phát triền phần mềm và tiết kiệm thời gian cho việc phát triển, vận hành dự án. Mục đích cuối cùng vẫn luôn là cung cấp cho người dùng cuối một trải nghiệm ổn định.

Source code project: Github

Bài viết được đăng bởi cùng tác giả tại Link

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 500

- vừa được xem lúc

Cài đặt WSL / WSL2 trên Windows 10 để code như trên Ubuntu

Sau vài ba năm mình chuyển qua code trên Ubuntu thì thật không thể phủ nhận rằng mình đã yêu em nó. Cá nhân mình sử dụng Ubuntu để code web thì thật là tuyệt vời.

0 0 374

- vừa được xem lúc

Đặt tên commit message sao cho "tình nghĩa anh em chắc chắn bền lâu"????

. Lời mở đầu. .

1 1 701

- vừa được xem lúc

Tìm hiểu về Resource Controller trong Laravel

Giới thiệu. Trong laravel, việc sử dụng các route post, get, group để gọi đến 1 action của Controller đã là quá quen đối với các bạn sử dụng framework này.

0 0 335

- vừa được xem lúc

Phân quyền đơn giản với package Laravel permission

Như các bạn đã biết, phân quyền trong một ứng dụng là một phần không thể thiếu trong việc phát triển phần mềm, dù đó là ứng dụng web hay là mobile. Vậy nên, hôm nay mình sẽ giới thiệu một package có thể giúp các bạn phân quyền nhanh và đơn giản trong một website được viết bằng PHP với framework là L

0 0 421

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 414