Practical Node.js (2018版) 第3章:测试/Mocha.js, Chai.js, Expect.js

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. 写让测试通过的代码
  3. 确认测试通过,并重复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开始监听,‘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增加一些页面。

 

posted @ 2019-01-13 10:39  Mr-chen  阅读(475)  评论(0编辑  收藏  举报