原文: http://mherman.org/blog/2015/09/10/testing-node-js-with-mocha-and-chai/#.ViO8oBArIlJ
为什么要测试?
在此之前了解为什么要测试非常重要.
通过下面的地址获取一个Node/Express的简单的CRUD应用:
$ git clone https://github.com/mjhea0/node-mocha-chai-tutorial.git
$ git checkout tags/v1
现在可以通过食用curl(或者Postman)来测试CRUD了:
- 添加一个新的blob
- 查看所有的blob
- 查看一个blob
- 更新一个blob
- 删除一个blob
这个过程十分乏味. 如果我们在这个应用中没添加一个新功能就需要手动做这样的测试, 不仅会占用大量的时间而且可能还会因为手误或忘记测试某个功能使得测试不可信. 所以我们需要自动测试, 有了自动测试我们可以在几秒钟内测试玩几百个这样的功能.
首先, 安装Mocha:
$ npm install -g mocha@2.3.1
结构
在应用的根目录创建一个文件夹test, 然后在这个test文件夹中创建一个test-server.js. 现在项目的结构如下:
├── package.json
├── server
│ ├── app.js
│ ├── models
│ │ └── blob.js
│ └── routes
│ └── index.js
└── test
└── test-server.js
现在, 在test-server.js中添加下面的代码:
describe('Blobs', function() {
it('should list ALL blobs on /blobs GET');
it('should list a SINGLE blob on /blob/<id> GET');
it('should add a SINGLE blob on /blobs POST');
it('should update a SINGLE blob on /blob/<id> PUT');
it('should delete a SINGLE blob on /blob/<id> DELETE');
});
describe用来分组测试. it语句包含了每个独立的测试用例.
逻辑
Chai是一个assertion库, 我们使用chai-http做一些HTTP请求, 然后测试响应:
使用下面的命令同时安装chai和chai-http:
$ npm install chai@3.2.0 chai-http@1.0.0 --save-dev
更新test-server.js如下:
var chai = require('chai');
var chaiHttp = require('chai-http');
var server = require('../server/app');
var should = chai.should();
chai.use(chaiHttp);
describe('Blobs', function() {
it('should list ALL blobs on /blobs GET');
it('should list a SINGLE blob on /blob/<id> GET');
it('should add a SINGLE blob on /blobs POST');
it('should update a SINGLE blob on /blob/<id> PUT');
it('should delete a SINGLE blob on /blob/<id> DELETE');
});
现在可以写测试了
测试-GET(all)
更新第一个it():
it('should list ALL blobs on /blobs GET', function(done) {
chai.request(server)
.get('/blobs')
.end(function(err, res){
res.should.have.status(200);
done();
});
});
我们传递了一个只用一个参数(done function)的匿名函数给it. 这个匿名函数的最后一行是done(). 这个测试十分简单, Get /blobs 判断响应的http status code是否是200.
简单的运行mocha就能测试了:
$ mocha
Blobs
Connected to Database!
GET /blobs 200 19.621 ms - 2
✓ should list ALL blobs on /blobs GET (43ms)
- should list a SINGLE blob on /blob/<id> GET
- should add a SINGLE blob on /blobs POST
- should update a SINGLE blob on /blob/<id> PUT
- should delete a SINGLE blob on /blob/<id> DELETE
1 passing (72ms)
4 pending
因为简单的判断HTTP状态码对我们判断是否返回整个blobs来说意义不大, 现在来添加更多的断言:
it('should list ALL blobs on /blobs GET', function(done) {
chai.request(server)
.get('/blobs')
.end(function(err, res){
res.should.have.status(200);
res.should.be.json;
res.body.should.be.a('array');
done();
});
});
测试-POST
当添加blob成功, 响应如下:
{
"SUCCESS": {
"__v": 0,
"name": "name",
"lastName": "lastname",
"_id": "some-unique-id"
}
}
现在我们来写测试:
it('should add a SINGLE blob on /blobs POST', function(done) {
chai.request(server)
.post('/blobs')
.send({'name': 'Java', 'lastName': 'Script'})
.end(function(err, res){
res.should.have.status(200);
res.should.be.json;
res.body.should.be.a('object');
res.body.should.have.property('SUCCESS');
res.body.SUCCESS.should.be.a('object');
res.body.SUCCESS.should.have.property('name');
res.body.SUCCESS.should.have.property('lastName');
res.body.SUCCESS.should.have.property('_id');
res.body.SUCCESS.name.should.equal('Java');
res.body.SUCCESS.lastName.should.equal('Script');
done();
});
});
Hooks
专门有一个test数据库并且插入一些样例数据用来测试. 我们可以使用beforeEach和afterEach hooks-用来在每一个单元测试运行之前添加一些样例数据, 运行之后删除这些样例数据.
使用Mocha来做这些特别简单!
在server文件夹下面创建一个配置文件_config.js, 使用它为测试指定一个不通的数据库uri:
var config = {};
config.mongoURI = {
development: 'mongodb://localhost/node-testing',
test: 'mongodb://localhost/node-test'
};
module.exports = config;
接下来更新app.js文件:
// *** config file *** //
var config = require('./_config');
// *** mongoose *** ///
mongoose.connect(config.mongoURI[app.settings.env], function(err, res) {
if(err) {
console.log('Error connecting to the database. ' + err);
} else {
console.log('Connected to Database: ' + config.mongoURI[app.settings.env]);
}
});
最后更新测试脚本:
process.env.NODE_ENV = 'test';
var chai = require('chai');
var chaiHttp = require('chai-http');
var mongoose = require("mongoose");
var server = require('../server/app');
var Blob = require("../server/models/blob");
var should = chai.should();
chai.use(chaiHttp);
describe('Blobs', function() {
Blob.collection.drop();
beforeEach(function(done){
var newBlob = new Blob({
name: 'Bat',
lastName: 'man'
});
newBlob.save(function(err) {
done();
});
});
afterEach(function(done){
Blob.collection.drop();
done();
});
...snip...
现在, 每个单元测试(it)运行之前数据库会被清空, 接下来会添加一个blob. 运行之后有会清空数据库.
测试-GET(all)
有了hooks的设置后, 我们修改get(all)的单元测试:
it('should list ALL blobs on /blobs GET', function(done) {
chai.request(server)
.get('/blobs')
.end(function(err, res){
res.should.have.status(200);
res.should.be.json;
res.body.should.be.a('array');
res.body[0].should.have.property('_id');
res.body[0].should.have.property('name');
res.body[0].should.have.property('lastName');
res.body[0].name.should.equal('Bat');
res.body[0].lastName.should.equal('man');
done();
});
});
测试-GET(single)
it('should list a SINGLE blob on /blob/<id> GET', function(done) {
var newBlob = new Blob({
name: 'Super',
lastName: 'man'
});
newBlob.save(function(err, data) {
chai.request(server)
.get('/blob/'+data.id)
.end(function(err, res){
res.should.have.status(200);
res.should.be.json;
res.body.should.be.a('object');
res.body.should.have.property('_id');
res.body.should.have.property('name');
res.body.should.have.property('lastName');
res.body.name.should.equal('Super');
res.body.lastName.should.equal('man');
res.body._id.should.equal(data.id);
done();
});
});
});
测试-PUT
it('should update a SINGLE blob on /blob/<id> PUT', function(done) {
chai.request(server)
.get('/blobs')
.end(function(err, res){
chai.request(server)
.put('/blob/'+res.body[0]._id)
.send({'name': 'Spider'})
.end(function(error, response){
response.should.have.status(200);
response.should.be.json;
response.body.should.be.a('object');
response.body.should.have.property('UPDATED');
response.body.UPDATED.should.be.a('object');
response.body.UPDATED.should.have.property('name');
response.body.UPDATED.should.have.property('_id');
response.body.UPDATED.name.should.equal('Spider');
done();
});
});
});
测试-删除
it('should delete a SINGLE blob on /blob/<id> DELETE', function(done) {
chai.request(server)
.get('/blobs')
.end(function(err, res){
chai.request(server)
.delete('/blob/'+res.body[0]._id)
.end(function(error, response){
response.should.have.status(200);
response.should.be.json;
response.body.should.be.a('object');
response.body.should.have.property('REMOVED');
response.body.REMOVED.should.be.a('object');
response.body.REMOVED.should.have.property('name');
response.body.REMOVED.should.have.property('_id');
response.body.REMOVED.name.should.equal('Bat');
done();
});
});
});