TDD and BDD for Node.js with Mocha
TDD测试驱动开发。自动测试代码。
BDD: behavior-driven development行为驱动开发,基于TDD。一种自然化的测试语言。
例如,使用expect(response.status).to.equal(200)代替了TDD的assert.equal(response.status, 200)
Mocha文档:https://mochajs.org/
chai: 官网文档: https://www.chaijs.com/
Node.js文档: https://nodejs.org/api/async_hooks.html
Mocha阮一峰的测试框架 Mocha 实例教程
Chai断言库可以看Chai.js断言库API中文文档
Mocha 摩卡☕️
(点击连接看git)(方法:文档)
本章介绍:比较流行的javascript test framework for Node.js和browser: mocha
涉及以下方面:
- 安装理解Mocha
- TDD的assert。
- BDD的expect.js模块。
- Project: 为上个章节的Blog app写一个测试。
Installing and Understanding Mocha(17000✨)
Mocha是成熟的强大的测试框架。全局安装
$ npm install –-global mocha
驱动测试开发基本步骤:
- 写测试
- 写让测试通过的代码
- 确认测试通过,并重复1,2步骤。
BDD是TDD的特色版本。它使用单元测试,逐步满足商业需求。
Node.js核心模块是assert。但是,使用明确的testing library更好。
本章使用Mocha测试框架,让许多事情变得更"free",因为Mocha可以:
- Reporting
- 异步支持
- 丰富的可配置性
- 通知Notifications
- Debugger 支持
- 常用的交互:before, after钩子
- 文件watcher支持。
使用Mocha有更多的功能和好处。这里有一个选项参数名单,使用$ mocha [options]命令。
全的list:mocha -h
例如:
mocha test-expect.js -R nyan
选择一个框架的类型,有很多选择。Mocha是作者推荐的一个,有17k✨。除此之外还有以下选择:
- Jest (https://facebook.github.io/jest):(2k✨) A framework for mostly React and browser testing, which is built on Jasmine and has a lot of things included
- Jasmine: (https://jasmine.github.io):(1.4k✨) A BDD framework for Node and browser testing, which follows Mocha notation
- Vows (http://vowsjs.org): (1.6k✨)A BDD framework for asynchronous testing
- Enzyme (1.6k?) : A language mostly for React apps, which has a jQuery-like syntax and is used with Mocha, Jasmine, or other test frameworks
- Karma :(1.0k✨) A testing framework mostly for Angular apps(vue也推荐使用,将项目运行在各个主流浏览器进行测试!)
Vue官方推荐使用Karma, mocha, chai.
Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试。
Mocha的常用命令和用法不算太多,看阮一峰老师的测试框架 Mocha 实例教程就可以大致了解了。
而Chai断言库可以看Chai.js断言库API中文文档,很简单,多查多用就能很快掌握。
Understanding Mocha Hooks
钩子:逻辑代码,一个函数或一些statements。当相关的event发送时,这类钩子被执行!(具体第7章,使用hook来探索Mongoose库)。
Mocha有一些钩子,会在不同的suite的部分被执行:在整个suite之前,在每个test之前,等等。
除了before, beforeEach
还有after, afterEach()钩子: 它们用于清除 testing setup, 例如一些被用于测试的数据库数据。
所有的hooks支持异步模型。test也一样。例如:
下面的testing suite 是同步的,并不会等响应response完成。
describe('homepage', () => { it('should respond to GET', () => { superagent .get(`http://localhost:${port}`) .end((error, response) => { expect(response.status).to.equal(200) // This will never happen }) }) })
(还可以使用axios推荐✅, Node.js的核心模块http)
⚠️:上面的代码,在执行测试时,会抛出❌,提示没有done()函数
原因分析:
因为传入的函数是异步的,所以要增加一个done参数给这个测试的函数。
我们调用done(),让Mocha(或者Jasmine,Jest)知道,“喂,你能够继续了,这里不会再有assert了。”
如果done()被忽略了,这个测试会报告❌time out,因为没让这个测试 runner/framework知道测试已经完成。
测试模块可以处理异步或同步的测试函数。
- 如果运行一个同步函数的测试。无需使用done()。
- 但是如果是运行一个异步函数的测试,需要使用done()告诉Mocha这个测试是异步的。
- 可选的,可以返回一个Promise。来代替done()回调函数。
- 如果你的JS环境支持async/await, 你也可以用它写异步测试。
修改上例子:
增加一个回调函数(通常是done)给it(), Mocha将知道它需要等待这个函数被调用,以完成测试。
这个回调函数可以接受一个Error instance, 或一个falsy value, done(err)会引起一个失败的测试。
describe('homepage', () => { it('should respond to GET', (done) => { superagent .get(`http://localhos:${port}`) .end((error, response) => { expect(response.status).to.equal(200) done() }) }) })
测试案例(describe)可以嵌套,并且hooks可以在不同的层级混入不同的测试案例。
嵌套的describe构建器,在大型测试文件,是一个好主意!
有时开发者想要忽略一个测试case/suite(describe.skip()或者it.skip())或者让它们排外的(describe.only())
。describe.only()意味着只运行这个指定的测试。
作为一个可选的BDD接口的describe,it, before等等,Mocha支持很多传统的TDD接口方法:
- suite = describe
- test = it
- setup = before
- teardown = after
- suiteSetup = beforeEach
- suiteTeardown = afterEach
TDD with the Assert
让我们用assert库写一个测试。这个库是Node.js的核心库。它有最小化的方法的设置,但对如单元测试来说足够了。
在安装好Mocha后:
新建测试文件夹:
mkdir test-example cd test-example touch test-assert.js
写一个简单的测试在test-assert.js内
const assert = require('assert') describe('String#split', () => { it('should return an array', () => { assert(Array.isArray("a,b,c".split(',')) }) })
运行并得到结果
$ mocha test-example/test-asssert.js //得到结果: String#split ✓ should return an array 1 passing (6ms)
备注⚠️:如果是在程序文件的本地安装的mocha, 需要提供本地的安装路径,必须这么写:
$ ./node_modules/.bin/mocha test-assert.js //当前目录是程序根目录
另外,如果使用的是windows系统:这么写:
$ node_modules\.bin\mocha test.js
我们增加2个变量到刚才的测试,增加一个测试案例:断言相等,使用for循环和assert.equal()方法
const assert = require('assert') const testArray = ['a','b','c'] const testString = 'a,b,c' describe('String#split', () => { it('should return an array', () => { assert(Array.isArray('a,b,c'.split(','))) }) it('should return the same array', () => { assert.equal(testArray.length, testString.split(',').length, `arrays have equal length`) for (let i = 0; i < testArray.length; i++) { assert.equal(testArray[i], testString.split(',')[i], `i element is equal`) } }) })
你可以看到,上面的代码有不少重复使用,因此我们把它们抽象一下,放入before和beforeEach构建器内。
一点抽象总是一件好事~(Abstraction仅仅是一个关于cut and paste的豪华的词语,一个软件设计团队喜欢使用它,仅仅为了高点的奖金?)
下面是一个这个测试的新版本,我们把种子数据抽象成current变量。
使用了before()和beforeEach()
var assert = require('assert') var expected, current before(() => { expected = ['a', 'b', 'c'] }) describe('String#split', () => { beforeEach(() => { current = 'a,b,c'.split(',') }) it('should return an array', () => { assert(Array.isArray(current)) }) it('should return the same array', () => { assert.equal(expected.length, current.length, 'arrays have equal length') for (let i = 0; i < expected.length; i++) { assert.equal(expected[i], current[i], `i element is equal`) } }) })
解释:
before(function() { // runs before all tests in this block });
beforeEach(function() { // runs before each test in this block });
Chai Assert (expect)
(点击查看:官方文档)
除了使用core库assert。 还有一个Chai库拥有assert module(也有expect module, should module)
开发者更爱使用Chai assert, 因为它有更多的功能!
Chai断言库可以看Chai.js断言库API中文文档,很简单,多查多用就能很快掌握。
安装
$ npm install chai
引入
const assert = require('chai').assert
或者使用ES6的语法:
const { assert } = require('chai')
上面的例子,第一行可以改成这行代码!
下面是常用的Chai方法:
assert(expressions, message): //如果expressions是false,抛出错误信息。 assert.fail(actual, expected, [message], [operator]): //抛出❌,同时显示actual, expected和operator的值。
assert.ok(object, [message]) //当对象不==true, truthy(0和空string是false的),会抛出❌。
assert.equal(actual, expected, [message]) //当actual,不==等于expected的时候,抛出错误
⚠️:chai.assert和Node.js的核心assert 模块并不完全一致,因为前者有更多的方法。同样,chai.expect和独立的express.js也不完全一样。 本文使用chai.expect
BDD with Expect
Expect是BDD语言。因为它的语法允许chaining,所以非常著名。 比核心模块assert的功能更好用。
语法是自然的可读可理解,包括开发者,质量检查工程师(测试工程师?)甚至程序管理员。
有2种Expect可以选择:
- Standalone: 需要安装expect.js模块
- Chai: 内置的(推荐✅)
还是上一个例子,代码:
expect(current.length).to.equal(3)
文档例子:
var expect = require('chai').expect , foo = 'bar' , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] }; expect(foo).to.be.a('string'); expect(foo).to.equal('bar'); expect(foo).to.have.lengthOf(3); expect(beverages).to.have.property('tea').with.lengthOf(3);
Express.js
再看一下express.js这个独立的库。它和chai.expect不完全一样。使用下面的命令安装:
$ npm install expect.js //引入模块 const expect = require('expect.js')
⚠️$npm i name需要在程序的根文件夹执行,根文件夹必须包括node_modules目录或者一个package.json文件。
作者说,一般只使用equal,ok,true等几个方法。
具体文档:
chai http://chaijs.com/api/bdd.
Express.js https://github.com/LearnBoost/expect.js.
Project: Writing the First BDD Test for Blog
这个mini-project的目标是为Blog增加少量测试。我不会进行(headless browsers)浏览器测试和UI测试, 这是扩展的题目了。
这里只发送一些HTTP请求并转化它们的response。
代码:https://github.com/azat-co/practicalnode/tree/master/code/ch3/blog-express
拷贝它。然后安装Mocha.然后安装superagent
$ npm install mocha@4.0.1 --save-dev
选项--save-dev:
package会出现在你的devDependencies内。即development dependency。这个包只在开发阶段使用。
相当于Ruby on Rails框架中的gem文件中,对gem的使用范围的分类。
安装axios, expect.js:
//package.js ... "dependencies": { "chai": "^4.2.0", "ejs": "^2.6.1", "express": "^4.15.4" }, "devDependencies": {
"expect.js": "0.3.1", "mocha": "4.0.1", "axios": "3.8.0"
}
}
创建文件test/index.js:
const boot = require('../app').boot const shutdown = require('../app').shutdown const port = require('../app').port
const axios = require('axios') const expect = require('chai').expect describe('server', () => { before(() => { boot() }) describe('homepage', () => { it('should respond to GET', (done) => { axios .get(`http://localhost:${port}`) .then(function(response) { expect(response.status).to.equal(200) done() }) .catch((err) => { done(err) }) }) }) after(() => { shutdown() }) })
⚠️done()必须有:
For async tests and hooks, ensure "done()" is called;
if returning a Promise, ensure it resolves.
然后进入app.js, the Express server in app.js
记住,测试中使用boot, shutdown,所有需要expose这2个方法。在app.js内,当app.js被其他文件进口时。
本案例,进口在test中完成test/indes.js。这让系统更灵活。
目标是让测试boot the server,并在无测试时也能开启server。
所以,不在直接使用listen()来发射app.js内的server:
http.createServer(app).listen(app.get('port'), () => { console.log(`Express server listening on port ${app.get('port')}`) })
重构它,加一个条件判断: require.main === module
- ❌出口server Express app object(false) for usage in the Mocha test file(test/index.js)
- ✅boot up the server right away(true)
我们将把listen()移动到boot()函数内,这样就可以直接地调用,或者拥有出口给其他的文件:
const express = require('express') const http = require('http') const path = require('path') const ejs = require('ejs') // Express使用一个函数模式,执行函数,得到一个实例。 let app = express() app.set('appName', 'hello-advanced') app.set('port', process.env.PORT || 3000 ) app.set('views', path.join(__dirname, 'views')) app.engine('html', ejs.__express) app.set('view engine', 'html') app.all('*', (req, res) => { res.render('index', {msg: 'Welcome to the Practical Node.js'}) }) //新增这些代码: const server = http.createServer(app) const boot = () => { server.listen(app.get('port'), () => { console.info(`Express server listening on port ${app.get('port')}`) console.log(`Express server listening on port ${app.get('port')}`) }) } const shutdown = () => { server.close() } if ( require.main === module ) { boot() // ?的代码用于判断是否是使用"node app.js"命令运行app.js脚本文件。 } else { console.log('Running app as a module') exports.boot = boot exports.shutdown = shutdown exports.port = app.get('port') }
代码解释:
require.main 对象。(见node.js官网文档Modules章节) Module对象代表了加载的entry script。 在entry.js脚本内,写代码: console.log(require.main) 在terminal执行命令:node entry.js 返回: Module { id: '.', exports: {}, parent: null, filename: '/Users/chen/node_practice/hello-world/entry.js', loaded: false, children: [], paths: [ '/Users/chen/node_practice/hello-world/node_modules', '/Users/chen/node_practice/node_modules', '/Users/chen/node_modules', '/Users/node_modules', '/node_modules' ] }
Accessing the main module
当一个文件直接地从Node.js运行,require.main命令被设置为它的module。
通过测试代码require.main === module,来判断一个文件是否是直接在terminal上输入命令node xxx运行的。
即一个文件如果是通过node name.js运行的,则require.main === module,
如果是通过require('./name')运行的,则requre.main 不等于moudle。
Modules
在Node.js模块系统,每个文件都是一个独立的模块module。
缓存:
Modules在第一次被加载后,存入缓存内。之后每次require()都会从缓存中得到。
Class: http.Server 这个类继承自net.Server。 并有一堆方法,如listen() http.createServer([options][, requestListener]) 返回一个http.Server的实例。
参数options是对象。
参数requestListener是函数。它自动的添加到request event
http.Server#server.listen()
类http.Server的实例方法。开始监听Http server的connections.
它等同于net.Server的server.listen()
net.Server#server.listen()
有基本的4种参数结构:
server.listen(handle[, backlog][, callback])
server.listen(options[, callback])
server.listen(path[, backlog][, callback])
for IPC serversserver.listen([port[, host[, backlog]]][, callback])
for TCP servers
这个函数是异步的,当server开始监听,‘listening’事件会被发出。
最后的参数callback将作为一个listener被添加。
server.close([callback])
停止server接收新的连接,并保持现在的连接。具体件net.Server.close()
异步函数,当所有连接被结束,server会被关闭。
callback当close event发生时调用。
返回<net.Server>实例
Express的方法get,有2种类型的参数,并导致不同的结果:
第一种:
app.get(name) 返回name app setting的值, name是app setting table的其中一个string. app.get('title'); // => undefined app.set('title', 'My Site'); app.get('title'); // => "My Site"
第二种:
app.get(path, callback [, callback ...]) Routes HTTP GET requests to the specified path with the specified callback functions.
运行测试得到结果:
$ mocha test/index.js Running app as a module server homepage Express server listening on port 3000 Express server listening on port 3000 ✓ should respond to GET 1 passing (34ms)
所以说,让测试来登陆boot up你的server是很方便的。你不需要记得先boot up服务器后,再运行测试。
用代码来简化步骤,Yes~!
Putting Configs into a Makefile(未看)
mocha命令接受许多选择options。把这些选项放在一个文件内是一个好主意。设定一个语法糖,一次性执行多行选项。具体见本文和下面的连接。
"Understanding Make" at http://www.cprogramming.com/tutorial/makefiles.html and "Using Make and Writing Makefiles" at http://www.cs.swarthmore.edu/~newhall/unixhelp/howto_makefiles.html.
Summary
本章安装Mocha,并学习使用它。我们用assert写简单的测试,了解chai.expect, chai.expect,expect.js库。
为Blog创建了一个测试,并修改app.js让它作为一个模块工作。
第10章,会讲解集成 service TravisCI,并在虚拟云环境内使用GigHub来激活继续的多重测试。
下一章介绍一个web app的关键: HTML输出--template engine。深挖Pug,Handlebars,并给Blog增加一些页面。