Mocha 是一种有丰富特性的Javascript测试框架,可以运行于Nodejs和浏览器环境中。 本文是使用Mocha进行Javascript单元测试的入门教程。

Mocha的特点有:

  1. 既可以测试简单的JavaScript函数,又可以测试异步代码,因为异步是JavaScript的特性之一;
  2. 可以自动运行所有测试,可以只运行特定的测试,也可以跳过部分测试;
  3. 支持before、after、beforeEach和afterEach来编写初始化和清理代码。

在本文中,我们将所有相关代码及文档放在文件夹mocha-js-ut-tutorial中,在文中,该文件夹目录亦称之为根目录或用/表示。文中相关代码请参见 mocha-js-ut-tutorial

Mocha初体验

安装Mocha

在安装Mocha前,请确认根目录中存在package.json文件。手动创建或npm init均可。

  1. npm install --save-dev mocha

一般我们只在开发阶段才会对代码进行单例测试,因此Mocha只需要在开发阶段中使用,将其安装到DevDependencies中即可。有些教程中会建议把Mocha安装到全局(-g)中,私以为没有必要。正式打包发布时,devDependencies的包不会被包含进来。

Hello World

1.在根目录创建hello-world.test.js

  1. const assert = require('assert');
  2. describe('hello world', () => {
  3. it('should pass the test', () => {
  4. assert.strictEqual(0, 0);
  5. assert.strictEqual(true, true);
  6. });
  7. });

2.然后在命令行中运行node_modules/mocha/bin/mocha hello-world.test.js,你将看到以下结果:

$ node_modules/mocha/bin/mocha hello-world.test.js —reporter spec

hello world
√ should pass the test

1 passing (6ms)

可以看到,基础用法是在mocha命令后面紧跟测试脚本的路径和文件名,可以指定多个测试脚本:$ mocha file1 file2 file3

如果在上述步骤中遇到错误,请参考本文后续章节ES6

使用Mocha对Javascript模块进行单元测试

在上一节中,我们通过一个hello-world程序对Mocha有了最初的认识,然而与在实际项目中的使用情况还有些差距。接下来我们将模拟一个实际项目并使用Mocha对其进行测试。

编写待测试模块源码

1.在根目录创建src目录
2.创建add.js

  1. module.exports = (a, b) => a + b;

3.创建minus.js

  1. module.exports = (a, b) => a - b;

编写模块化测试代码

对Javascript模块进行单元测试时,我们一般会创建test目录,并将所有测试文件放在test目录中。Mocha默认运行test目录下的测试文件,这样在执行测试命令mocha时就不需要加上待测试文件名称了。另外,在编写测试代码时,一般测试代码与源代码模块一一对应。

1.在根目录创建test目录
2.创建add.test.js

  1. const assert = require('assert');
  2. const add = require('../src/add');
  3. describe('Test add() function', function() {
  4. it('should equals to 2 by add 1 to 1', function() {
  5. assert.strictEqual(add(1, 1), 2);
  6. });
  7. it('should equals to 0 by add -1 to 1', function() {
  8. assert.strictEqual(add(-1, 1), 0);
  9. });
  10. });

3.创建minus.test.js

  1. const assert = require('assert');
  2. const minus = require('../src/minus');
  3. describe('Test minus() function', function() {
  4. it('should equals to 1 by minusing 1 from 2', function() {
  5. assert.strictEqual(minus(2, 1), 1);
  6. });
  7. });

4.运行命令node_modules/mocha/bin/mocha,将看到以下结果:

$ node_modules/mocha/bin/mocha —reporter spec
Test add() function
√ should equals to 2 by adding 1 to 1
√ should equals to 0 by adding -1 to 1

Test minus() function
√ should equals to 1 by minusing 1 from 2

3 passing (7ms)

npm test script

package.jsonscripts中添加一条test命令:

  1. {
  2. ...
  3. "scripts": {
  4. "test": "mocha"
  5. }
  6. ...
  7. }

这样在与命令行中使用npm test即可运行Mocha,即使Mocha没有全局安装也可以正常运行。

断言

Mocha允许使用任意断言库。在前面的例子中,我们使用的是Nodejs内置的[assert](https://nodejs.org/api/assert.html)模块。assert模块非常简单,它断言一个表达式为true。如果断言失败,就抛出Error。我们还可以使用以下第三方断言库:

  1. chai
    强大的断言库,BDD/TDD两种风格均支持,同时支持should/expect/assert三种断言风格。且有强大的插件机制。
  2. should.js BDD风格断言库
  3. expect.js expect()风格断言库
  4. better-assert C-style self-documenting assert()
  5. unexpected 可扩展的BDD风格断言工具包

异步测试

参考 Mocha官方文档 Asynchronous Code

同步测试

参考 Mocha官方文档 Synchronous Code

lambdas 箭头函数

在Mocha中使用lambdas箭头函数是让人沮丧的。由于从词法上来说箭头函数会绑定this的值,因此这样的函数将无法访问Mocha的上下文(Context),如下:

  1. describe('my suite', function() {
  2. it('my test', function() {
  3. // should set the timeout of this test to 1000 ms; instead will fail
  4. // 本意是想设定这个测试用例的超时时间为1s,但这里将会报错。timeout是Mocha Context的方法
  5. this.timeout(1000);
  6. assert.ok(true);
  7. });
  8. });

如果你不需要使用Mocha Context,你可以正常使用lambdas 箭头函数。然而,如果你使用了lambdas并且有一天你需要访问Mocha Context,重构的难度将会更大。

HOOKS

HOOKS不好翻译。在默认的BDD风格接口中,Mocha提供了before()``after()``beforeEach()``afterEach()四个方法。这些方法可以用来在测试前初始化前置条件或者在测试结束后执行清理工作。

  1. describe('hooks', function() {
  2. before(function() {
  3. // runs before all tests in this block
  4. });
  5. after(function() {
  6. // runs after all tests in this block
  7. });
  8. beforeEach(function() {
  9. // runs before each test in this block
  10. });
  11. afterEach(function() {
  12. // runs after each test in this block
  13. });
  14. // test cases
  15. });

测试用例可以写在任意位置(在HOOKS之前、在HOOKS之后、穿插在HOOKS之间)。不论书写位置如何,所有的HOOKS都可以按合理的顺序运行。先执行before()(且只执行一次);然后对每个测试用例先执行beforeEach,接下来运行测试用例,再运行afterEach;最后所有测试用例测试结束后,执行after()(只执行一次)。【猜测】:同一种HOOK可以写多个,多个相同类型的HOOKS的执行顺序由他们定义的相对位置顺序决定。

HOOKS的描述

HOOk可以指定一个可选的描述,根据这个描述可以更容易地在测试中定位错误。如果HOOK被指定了函数名,在输出中将使用这个函数名(见样例2)。

  1. beforeEach(function() {
  2. // beforeEach hook
  3. });
  4. beforeEach(function namedFun() {
  5. // beforeEach:namedFun
  6. });
  7. beforeEach('some description', function() {
  8. // beforeEach:some description
  9. });

ROOT-LEVEL HOOKS

在最外层的describe()之外也可以使用HOOKS,因为Mocha Context实现了describe(),这样的HOOK就是ROOT-LEVEL HOOK,对于Mocha的describe,我们称之为“root suite”。

待编写(PENDING)的测试用例

  1. it('should ...');

没有回调函数。Someone should write these test cases eventually.

单独(EXCLUSIVE)运行部分测试用例

在测试集(suite)或者测试用例(case)后面使用.only()既可以在测试时只运行部分suites或cases。其内部级联的suites和cases也会被执行(除非内部还有其他.only())。

  1. describe('test', function() {
  2. describe.only('test this', function() {
  3. it.only('test this case', function() {
  4. });
  5. it('this case will be skipped', function() {
  6. });
  7. });
  8. describe('this suite will be skipped', function() {
  9. });
  10. });

>=V3.0.0中,.only()可以使用多次。.only()对HOOKS无影响,HOOKS总是可以被执行。

跳过(INCLUSIVE)部分测试用例

.only()相对应的用法,在suite或case后面使用.skip()来跳过(略过)某个测试集或测试用例。被跳过的suite和case将被标为PENDING。

  1. describe('Array', function() {
  2. describe('#indexOf()', function() {
  3. it.skip('should return -1 unless present', function() {
  4. // this test will not be run
  5. });
  6. it('should return the index when present', function() {
  7. // this test will be run
  8. });
  9. });
  10. describe.skip('#splice()', function() {
  11. // this suite will not be run
  12. });
  13. });

BEST PRACTICE 最佳实践

Use .skip() instead of commenting tests out
如果不想执行某些测试用例,使用.skip()而不是把它们注释掉

this.skip()

在运行时也可以使用this.skip()来中止跳过测试。场景:有时候测试用例必须在特定环境或配置下运行,且环境和配置不能事先预知,只能在运行时获得。在before()HOOK中使用this.skip()可以跳过整个suite。

  1. it('should only test in the correct environment', function() {
  2. if (/* check test environment or configuration */) {
  3. // make assertions
  4. } else {
  5. this.skip(); // good
  6. // do nothing, bad
  7. }
  8. });

BEST PRACTICES 最佳实践

  1. To avoid confusion, do not execute further instructions in a test or hook after calling this.skip().
    调用this.skip()之后,不要再执行更多指令了。
  2. Don’t do nothing! A test should make an assertion or use this.skip().
    不要出现什么语句都没有的逻辑代码块,一个测试用例应该要么做出断言,要么使用this.skip()跳过测试。

重试(RETRY)

这个功能主要是为端到端(functional tests/Selenium…)的测试设计的,可以对某个测试用例尝试执行多次。不建议在单元测试(unit tests)中使用这个功能。这个功能会重新执行befreEach()/afterEach(),但不会重复执行before()/after()

仅供参考:written using Selenium webdriver

  1. describe('retries', function() {
  2. // Retry all tests in this suite up to 4 times
  3. this.retries(4);
  4. beforeEach(function () {
  5. browser.get('http://www.yahoo.com');
  6. });
  7. it('should succeed on the 3rd try', function () {
  8. // Specify this test to only retry up to 2 times
  9. this.retries(2);
  10. expect($('.foo').isDisplayed()).to.eventually.be.true;
  11. });
  12. });

REPORTERS 测试报告(生成器)

Mocha提供多种风格的测试报告,同时也可以使用第三方的测试报告样式。可以使用参数--reporter <name>来指定报告样式。

  1. SPEC --reporter spec 默认的报告样式

    SPEC

    SPEC ERROR

  2. DOT MATRIX --reporter dot 使用一系列的点来表示测试结果。红色的叹号!表示失败,蓝色的逗号,表示待实现(PENDING),黄色的点.表示测试慢,白色的点.表示成功。如果你喜欢最简洁的输出可以使用这种报告样式。

    DOT

  3. NYAN --reporter nyan 一只猫

    NYAN

  4. TAP --reporter tap The TAP reporter emits lines for a Test-Anything-Protocol consumer.

    TAP

  5. LANDING STRIP --reporter landing 模仿飞机降落

    LANDING

    LANDING ERROR

  6. LIST --reporter list 输出简洁的测试通过或失败的结果,并在最底部输出测试失败的细节。

    LIST

  7. PROGRESS --reporter progress 模拟简单的进度条

    PROGRESS

  8. JSON --reporter json 在所有测试结束时输出一个JSON对象(包含所有测试细节)

    JSON

  9. JSON STREAM 实时地按照事件的发生顺序输出以新行(newline)为界定的JSON事件,以start事件开始,然后是各个测试的测试结果,最后是end事件。

    JSON STREAM

  10. MIN --reporter min 只输出测试结果汇总,当然也会输出错误和失败。MIN样式和--watch搭配在一起使用非常合适,它们可以清空terminal终端并把测试汇总输出在终端的最上方。

    MIN

  11. DOC --reporter doc 输出级联的HTML body内容,使用这些内容,可以很容易地生成好看的文档。(Example: SuperAgent request library

    DOC

  12. MARKDOWN --reporter markdown 输出Markdown格式的TOC导航和内容。如果你想用测试的输出来作为Github wiki文档,或者放在repository下面让Github来渲染,这种样式是不错的选择。(Example:Connect

  13. HTML 目前Mocha支持的唯一一种浏览器报告。

    HTML

  14. UNDOCUMENTED REPORTERS 非正式?

    The “XUnit” reporter is also available. By default, it will output to the console. To write directly to a file, use --reporter-options output=filename.xml.

  15. 第三方报告生成器

    Mocha allows you to define custom third-party reporters. For more information see the wiki. Examples:

THE TEST/ DIRECTORY

Mocha默认在./test目录下找jscoffee文件来执行,因此应该将测试文件放在test目录下。

Mocha命令参数

—recursive

mocha命令默认只执行test目录(不包含其子目录)下的测试文件,加上--recursive参数可以改变这种行为,从而递归执行test及其子孙目录下的测试文件。

MOCHA.OPTS

Mocha会尝试加载./test/mocha.opts文件并使用其中的配置作为运行参数,我们可以把任意命令行参数放在这个文件中。另外,命令行参数优先级高于mocha.opts文件中的参数。如:

  1. // mocha.opts
  2. --reporter DOT
  3. --ui bdd

$ mocha --reporter list --growl 该命令会覆盖mocha.opts中的reporter参数,并且启用Growl,使用BDD格式接口。

MORE

参见 Mocha:

  • DYNAMICALLY GENERATING TESTS
  • TEST DURATION
  • TIMEOUTS
  • DIFFS
  • INTERFACES
  • RUNNING MOCHA IN THE BROWSER
  • EDITOR PLUGINS
  • EXAMPLES

参考

Mocha
Chai
测试框架 Mocha 实例教程 - 阮一峰
Javascript教程-Mocha - 廖雪峰