前言

什么是自动化测试
  • 自动化测试在很多团队中都是Devops环节中很难执行起来的一个环节,主要原因在于测试代码的编写工作很难抽象,99%的场景都需要和业务强绑定,而且写测试代码的编写工作量往往比编写实际业务代码的工作量更多。在一些很多业务场景中投入产出比很低,适合写自动化测试的应该是那些中长期业务以及一些诸如组件一样的基础库。自动化测试是个比较大的概念,其中分类也比较多,比如单元测试,端对端测试,集成测试等等,其中单元测试相对而言是我们比较耳熟能详的一个领域。单元测试框架有很多,比如Mocha,Jest,AVA等。Mocha是我们今天文章的重点,我们先来了解下mocha是怎样的一款框架。

什么是Mocha
  • Mocha是一款运行在nodejs上的测试框架,相信大家或多或少都有听过或是见过,支持同步和异步测试,同时还支持TDD,BDD等多种测试风格,mocha作为一款老牌的测试框架已经被广泛应用在单元测试或是端对端测试的场景中。mocha的源码十分的冗长,而且包含了很多的高级玩法,但实际上mocha的核心原理是十分简单的,导致源码体积庞杂的原因主要在于实现了很多其他的功能,做了很多代码上的兼容处理。比如生成html格式的测试报告这种,支持多种的测试风格,插件系统等等。但实际在业务中我们对mocha本身90%的场景的使用也仅仅是他的“测试”功能而已。诸如多种文本格式的测试覆盖率报告的生成,断言库,测试数据mock等等其它功能都可以使用做的更好一些第三方库来代替。mocha本身是个比较纯粹的测试框架。

准备

了解mocha
  • 综上所述,撇弃mocha其它的复杂实现,针对于它的核心原理的解读是本次分享的主题。源码阅读十分枯燥,我们将根据目前现有的mocha核心功能实现一个简易的mocha。在此之前我们先认识下如何使用mocha,下面是一段来自lodash判断数据类型的代码:

// mocha-demo/index.js
const toString = Object.prototype.toString;

function getTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  return toString.call(value)
}

module.exports = {
  getTag,
};

上述代码使用了Object.prototype.toString来判断了数据类型,我们针对上述代码的测试用例(此处断言使用node原生的assert方法,采用BDD的测试风格):

// test/getTag.spec.js
const assert = require('assert');
const { getTag } = require('../index');

describe('检查:getTag函数执行', function () {
  before(function() {
    console.log('😁before钩子触发');
  });
  describe('测试:正常流', function() {
    it('类型返回: [object JSON]', function (done) {
      setTimeout(() => {
        assert.equal(getTag(JSON), '[object JSON]');
        done();
      }, 1000);
    });
    it('类型返回: [object Number]', function() {
      assert.equal(getTag(1), '[object Number]');
    });
  });
  describe('测试:异常流', function() {
    it('类型返回: [object Undefined]', function() {
      assert.equal(getTag(undefined), '[object Undefined]');
    });
  });
  after(function() {
    console.log('😭after钩子触发');
  });
});

mocha提供的api语义还是比较强的,即使没写过单元测试代码,单看这段代码也不难理解这段代码干了啥,而这段测试代码页会作为我们最后验证简易Mocha的样例,我们先来看下使用mocha运行该测试用例的执行结果:

d0f72523adafb6bdec86ecfd381216e1.png

如上图所示,即我们前面测试代码的执行结果,我们来拆分下当前mocha实现的一些功能点。

注:mocha更多使用方法可参考Mocha - the fun, simple, flexible JavaScript test framework[1]

核心函数
  • 首先我们可以看到mocha主要提供两个核心函数 describe it来进行测试用例的编写。describe函数我们称之为测试套件,它的核心功能是来描述测试的流程,it函数我们称之为一个测试单元,它的功能是来执行具体的测试用例。

测试风格
  • 上面的测试用例编写我们采用了典型的BDD风格,所谓的BDD风格可以理解为需求先行的一种测试风格,还有一种比较常见的测试风格TDD即测试驱动开发,TDD强调的是测试先行。在具体的业务开发中我们可以理解为TDD是指在写具体的业务的代码之前先写好测试用例,用提前编写好的测试用例去一步步完善我们的业务代码,遵循着测试用例->编码 -> 验证 -> 重构的过程,而BDD是指针对既有的业务代码进行编写测试用例,强调的是行为先行,使得测试用例覆盖业务代码所有的case。mocha默认采用的是BDD的测试风格,而且我们在实际开发中,更多涉及的其实也是BDD的测试风格,因此我们此次也将实现BDD的测试风格

钩子函数
  • 如上在执行测试套件或是测试单元之前mocha提供了很多的钩子:

  • before:在执行测试套件之前触发该钩子;

  • after:在测试套件执行结束之后触发该钩子;

  • beforeEach:在每个测试单元执行之前触发该钩子;

  • afterEach:在每个测试单元执行结束后触发该钩子;

    • 钩子的使用场景更多是在实际的业务场景中进行mock数据、测试数据收集、测试报告的自定义等;因此钩子也是mocha的核心功能之一

支持异步
  • 如上第一个测试用例:

it('类型返回: [object JSON]', function (done) {
  setTimeout(() => {
    assert.equal(getTag(JSON), '[object JSON]');
    done();
  }, 1000);
});

这种异步代码在我们实际业务中也是十分常见的,比如某一部分代码依赖接口数据的返回,或是对某些定时器进行单测用例的编写。mocha支持两种方式的异步代码,一种是回调函数直接返回一个Promise,一种是支持在回调函数中传参数done,手动调用done函数来结束用例。

执行结果和执行顺序
  • 我们可以看到用例的执行是严格按照从外到里,从上到下的执行顺序来执行,其中钩子的执行顺序和它的编写顺序无关,而且我们发现在测试用例编写过程中,诸如describeitbefore/after都无需引用依赖,直接调用即可,因此我们还要实现下相关 api 的全局挂载

设计

目录结构设计
├── index.js            #待测试代码(业务代码)
├── mocha               #简易mocha所在目录
│   ├── index.js       #简易mocha入口文件
│   ├── interfaces     #存放不同的测试风格
│   │   ├── bdd.js    #BDD 测试风格的实现
│   │   └── index.js  #方便不同测试风格的导出
│   ├── reporters      #生成测试报告
│   │   ├── index.js  
│   │   └── spec.js  
│   └── src            #简易mocha核心目录
│       ├── mocha.js   #存放Mocha类控制整个流程
│       ├── runner.js  #Runner类,辅助Mocha类执行测试用例
│       ├── suite.js   #Suite类,处理describe函数
│       ├── test.js    #Test类,处理it函数
│       └── utils.js   #存放一些工具函数
├── package.json
└── test               #测试用例编写
    └── getTag.spec.js

上面的mocha文件夹就是我们将要实现的简易版mocha目录,目录结构参考的mocha源码,但只采取了核心部分目录结构。

总体流程设计
  • 首先我们需要一个整体的Mocha类来控制整个流程的执行:

class Mocha {
  constructor() {}
  run() {}
}
module.exports = Mocha;

入口文件更新为:

// mocha-demo/mocha/index.js
const Mocha = require('./src/mocha');
const mocha = new Mocha();
mocha.run();

测试用例的执行过程顺序尤其重要,前面说过用例的执行遵循从外到里,从上到下的顺序,对于describeit的回调函数处理很容易让我们想到这是一个树形结构,而且是深度优先的遍历顺序。简化下上面的用例代码:

describe('检查:getTag函数执行', function () {
  describe('测试:正常流', function() {
   it('类型返回: [object JSON]', function (done) {
      setTimeout(() => {
        assert.equal(getTag(JSON), '[object JSON]');
        done();
      }, 1000);
    });
    it('类型返回: [object Number]', function() {
      assert.equal(getTag(1), '[object Number]');
    });
  });
  describe('测试:异常流', function() {
    it('类型返回: [object Undefined]', function() {
      assert.equal(getTag(undefined), '[object Undefined]');
    });
  });
});

针对这段代码结构如下:

a76c662dbcf351c8537f4dcca3b14392.png
image.png

整个树的结构如上,而我们在处理具体的函数的时候则可以定义Suite/Test两个类来分别描述describe/it两个函数。可以看到describe函数是存在父子关系的,关于Suite类的属性我们定义如下:

// mocha/src/suite.js
class  Suite {
  /**
*
* @param { * } parent 父节点
* @param { * } title Suite名称,即describe传入的第一个参数
*/  
 constructor ( parent, title ) {
 this . title = title; // Suite名称,即describe传入的第一个参数
 this . parent = parent // 父suite
this . suites = [];  // 子级suite
 this . tests = []; // 包含的it 测试用例方法
 this . _beforeAll = []; // before 钩子
 this . _afterAll = []; // after 钩子
 this . _beforeEach = []; // beforeEach钩子
 this . _afterEach = []; // afterEach 钩子
    // 将当前Suite实例push到父级的suties数组中
 if (parent instanceof  Suite ) {
parent. suites . push ( this );
}
}
}

module . exports = Suite ;

而Test类代表it就可以定义的较为简单:

// mocha/src/test.js
class Test {
  constructor(props) {
    this.title = props.title;  // Test名称,it传入的第一个参数
    this.fn = props.fn;        // Test的执行函数,it传入的第二个参数
  }
}

module.exports = Test;

此时我们整个流程就出来了:

  1. 收集用例(通过Suite和Test类来构造整棵树);

  2. 执行用例(遍历这棵树,执行所有的用例函数);

  3. 收集测试用例的执行结果。

    1. 此时我们整个的流程如下(其中执行测试用例和收集执行结果已简化):

dd08a84e06614623a0bf7846a2b73b80.png
image.png

OK,思路已经非常清晰,实现一下具体的代码吧

实现

创建根节点
  • 首先我们的测试用例树要有个初始化根节点,在Mocha类中创建如下:

// mocha/src/mocha.js
const Suite = require('./suite');
class Mocha {
  constructor() {
    // 创建根节点
    this.rootSuite = new Suite(null, '');
  }
  run() { }
}
module.exports = Mocha;
api全局挂载
  • 实际上Mocha为BDD 测试风格提供了 describe()、context()、it()、specify()、before()、after()、beforeEach() 和 afterEach()共8个api,其中context仅仅是describe的别名,主要作用是为了保障测试用例编写的可读性和可维护性,与之类似specify则是it的别名。我们先将相关api初始化如下:

// mocha/interfaces/bdd.js
// context是我们的上下文环境,root是我们的树的根节点
module.exports = function (context, root) {
  // context是describe的别名,主要目的是处于测试用例代码的组织和可读性的考虑
  context.describe = context.context = function(title, fn) {}
  // specify是it的别名
  context.it = context.specify = function(title, fn) {}
  context.before = function(fn) {}
  context.after = function(fn) {}
  context.beforeEach = function(fn) {}
  context.afterEach = function(fn) {}
}

为方便支持各种测试风格接口我们进行统一的导出:

// mocha/interfaces/index.js
'use strict';
exports.bdd = require('./bdd');

然后在Mocha类中进行bdd接口的全局挂载:

// mocha/src/mocha.js
const interfaces = require('../interfaces');
class Mocha {
  constructor() {
    // this.rootSuite = ...
    // 注意第二个参数是我们的前面创建的根节点,此时
    interfaces['bdd'](global, this.rootSuite "'bdd'");
  }
  run() {}
}

module.exports = Mocha;

此时我们已经完成了api的全局挂载,可以放心导入测试用例文件让函数执行了。

导入测试用例文件
  • 测试用例文件的导入mocha的实现比较复杂,支持配置,支持终端调用,也有支持CJS的实现,也有支持 ESM的实现,另外还有预加载,懒加载的实现,以满足在不同场景下测试用例的执行时机。我们此处简单的将测试用例文件的路径写死即可,直接加载我们本地使用的测试用例文件:

// mocha/src/utils.js
const path = require('path');
const fs = require('fs');

/**
*
* @param { * } filepath 文件或是文件夹路径
* @returns 所有测试文件路径数组
*/
module.exports.findCaseFile = function (filepath) {
  function readFileList(dir, fileList = []) {
    const files = fs.readdirSync(dir);
    files.forEach((item, _ ) => {
        var fullPath = path.join(dir, item);
        const stat = fs.statSync(fullPath);
        if (stat.isDirectory()) {      
            readFileList(path.join(dir, item), fileList);  // 递归读取文件
        } else {                
            fileList.push(fullPath);                     
        }        
    });
    return fileList;
  }
  let fileList = [];
  // 路径如果是文件则直接返回
  try {
    const stat = fs.statSync(filepath);
    if (stat.isFile()) {
      fileList = [filepath];
      return fileList;
    }
    readFileList(filepath, fileList);
  } catch(e) {console.log(e)}

  return fileList;
}

上面函数简单的实现了一个方法,用来递归的读取本地所有的测试用例文件,然后在Mocha类中使用该方法加载我们当前的测试用例文件:

// mocha/src/mocha.js
const path = require('path');
const interfaces = require('../interfaces');
const utils = require('./utils');
class Mocha {
  constructor() {
    // this.rootSuite = ...
    // interfaces['bdd'](global, this.rootSuite "'bdd'");
    // 写死我们本地测试用例所在文件夹地址
    const spec = path.resolve(__dirname, '../../test');
    const files = utils.findCaseFile(spec);
    // 加载测试用例文件
    files.forEach(file => require(file));
  }
  run() {}
}

module.exports = Mocha;
创建Suite-Test树
  • 到这一步我们的测试用例文件已经加载进来了,而describe和it函数也都已经执行,但我们上面的describeit还都是个空函数,我们接下来修改下我们提供的describe和it函数,来创建我们需要的树形结构,在前面我们已经在bdd.js文件中对describe和it进行了初始化,此时补充上我们借用栈创建Suite-Test树的逻辑:

// mocha/interfaces/bdd.js

const Suite = require('../src/suite');
const Test = require('../src/test');

module.exports = function (context, root) {
  // 树的根节点进栈
  const suites = [root];
  // context是describe的别名,主要目的是处于测试用例代码的组织和可读性的考虑
  context.describe = context.context = function (title, callback) {
    // 获取当前栈中的当前节点
    const cur = suites[0];
    // 实例化一个Suite对象,存储当前的describe函数信息
    const suite = new Suite(cur, title);
    // 入栈
    suites.unshift(suite);
    // 执行describe回调函数
    callback.call(suite);
    // Suite出栈
    suites.shift();
  }
  context.it = context.specify = function (title, fn) {
    // 获取当前Suite节点
    const cur = suites[0];
    const test = new Test(title, fn);
    // 将Test实例对象存储在tests数组中
    cur.tests.push(test);
  }
  // ...
}

注意,上面的代码我们仅仅是通过执行describe的回调函数将树的结构创建了出来,里面具体的测试用例代码(it的回调函数)还未开始执行。基于以上代码,我们整个Suite-Test树就已经创建出来了,截止到目前的代码我们收集用例的过程已经实现完成。此时我们的Sute-Test树创建出来是这样的结构:

a4912d9900c76f845b3817fee57f0e4f.png
image.png
支持异步
  • 前面说过,mocha支持异步代码的用例编写,异步代码的支持也很简单,我们可以在代码内部实现一个Promise适配器,将所有的 测试用例 所在的回调函数包裹在适配器里面,Promise适配器实现如下:

// mocha/src/utils.js
const path = require('path');
const fs = require('fs');

// module.exports.findCaseFile = ...

module.exports.adaptPromise = function(fn) {
  return () => new Promise(resolve => {
    if (fn.length === 0) {
      // 不使用参数 done
      try {
        const ret = fn();
        // 判断是否返回promise
        if (ret instanceof Promise) {
          return ret.then(resolve, resolve);
        } else {
          resolve();
        }
      } catch (error) {
        resolve(error);
      }
    } else {
      // 使用参数 done
      function done(error) {
        resolve(error);
      }
      fn(done);
    }
  })
}

我们改造下之前创建的Suite-Test树,将it、before、after、beforeEach和afterEach的回调函数进行适配:

// mocha/interfaces/bdd.js
const Suite = require('../src/suite');
const Test = require('../src/test');
const { adaptPromise } = require('../src/utils');

module.exports = function (context, root) {
  const suites = [root];
  // context是describe的别名,主要目的是处于测试用例代码的组织和可读性的考虑
  // context.describe = context.context = ...
  context.it = context.specify = function (title, fn) {
    const cur = suites[0];
    const test = new Test(title, adaptPromise(fn));
    cur.tests.push(test);
  }
  context.before = function (fn) {
    const cur = suites[0];
    cur._beforeAll.push(adaptPromise(fn));
  }
  context.after = function (fn) {
    const cur = suites[0];
    cur._afterAll.push(adaptPromise(fn));
  }
  context.beforeEach = function (fn) {
    const cur = suites[0];
    cur._beforeEach.push(adaptPromise(fn));
  }
  context.afterEach = function (fn) {
    const cur = suites[0];
    cur._afterEach.push(adaptPromise(fn));
  }
}
执行测试用例
  • 以上我们已经实现了所有收集测试用例的代码,并且也支持了异步,对测试用例的执行比较复杂我们可以单独创建一个Runner类去实现执行测试用例的逻辑:

// mocha/src/runner.js
class Runner {}

此时梳理下测试用例的执行逻辑,基于以上创建的Suite-Test树,我们可以对树进行一个遍历从而执行所有的测试用例,而对于异步代码的执行我们可以借用async/await来实现。此时我们的流程图更新如下:

aede4a2762f158193d89538771b41edc.png
image.png

整个思路梳理下来就很简单了,针对Suite-Test树,从根节点开始遍历这棵树,将这棵树中所有的Test节点所挂载的回调函数进行执行即可。相关代码实现如下:

// mocha/src/runner.js
class Runner {
  constructor() {
    super();
    // 记录 suite 根节点到当前节点的路径
    this.suites = [];
  }
  /*
* 主入口
*/
  async run(root) {
    // 开始处理Suite节点
    await this.runSuite(root);
  }
  /*
* 处理suite
*/
  async runSuite(suite) {
    // 1.执行before钩子函数
    if (suite._beforeAll.length) {
      for (const fn of suite._beforeAll) {
        const result = await fn();
      }
    }
    // 推入当前节点
    this.suites.unshift(suite);
    
    // 2. 执行test
    if (suite.tests.length) {
      for (const test of suite.tests) {
        // 执行test回调函数
        await this.runTest(test);
      }
    }
  
    // 3. 执行子级suite
    if (suite.suites.length) {
      for (const child of suite.suites) {
        // 递归处理Suite
        await this.runSuite(child);
      }
    }
  
    // 路径栈推出节点
    this.suites.shift();
  
    // 4.执行after钩子函数
    if (suite._afterAll.length) {
      for (const fn of suite._afterAll) {
        // 执行回调
        const result = await fn();
      }
    }
  }
  
  /*
* 处理Test
*/
  async runTest(test) {
    // 1. 由suite根节点向当前suite节点,依次执行beforeEach钩子函数
    const _beforeEach = [].concat(this.suites).reverse().reduce((list, suite) => list.concat(suite._beforeEach), []);
    if (_beforeEach.length) {
      for (const fn of _beforeEach) {
        const result = await fn();
      }
    }
    // 2. 执行测试用例
    const result = await test.fn();
    // 3. 由当前suite节点向suite根节点,依次执行afterEach钩子函数
    const _afterEach = [].concat(this.suites).reduce((list, suite) => list.concat(suite._afterEach), []);
    if (_afterEach.length) {
      for (const fn of _afterEach) {
        const result = await fn();
      }
    }
  }
}
module.exports = Runner;

将Runner类注入到Mocha类中:

// mocha/src/mocha.js
const Runner = require('./runner');

class Mocha {
  // constructor()..
  run() {
    const runner = new Runner();
    runner.run(this.rootSuite);
  }
}

module.exports = Mocha;

简单介绍下上面的代码逻辑,Runner类包括两个方法,一个方法用来处理Suite,一个方法用来处理Test,使用栈的结构遍历Suite-Test树,递归处理所有的Suite节点,从而找到所有的Test节点,将Test中的回调函数进行处理,测试用例执行结束。但到这里我们会发现,只是执行了测试用例而已,测试用例的执行结果还没获取到,测试用例哪个通过了,哪个没通过我们也无法得知。

收集测试用例执行结果

ceee497afa2b841046fef37f84f13971.png我们需要一个中间人来记录下执行的结果,输出给我们,此时我们的流程图更新如下:

修改Runner类,让它继承EventEmitter,来实现事件的传递工作:

// mocha/src/runner.js
const EventEmitter = require('events').EventEmitter;

// 监听事件的标识
const constants = {
  EVENT_RUN_BEGIN: 'EVENT_RUN_BEGIN',      // 执行流程开始
  EVENT_RUN_END: 'EVENT_RUN_END',          // 执行流程结束
  EVENT_SUITE_BEGIN: 'EVENT_SUITE_BEGIN',  // 执行suite开始
  EVENT_SUITE_END: 'EVENT_SUITE_END',      // 执行suite结束
  EVENT_FAIL: 'EVENT_FAIL',                // 执行用例失败
  EVENT_PASS: 'EVENT_PASS'                 // 执行用例成功
}

class Runner extends EventEmitter {
  // ...
  /*
* 主入口
*/
  async run(root) {
    this.emit(constants.EVENT_RUN_BEGIN);
    await this.runSuite(root);
    this.emit(constants.EVENT_RUN_END);
  }

  /*
* 执行suite
*/
  async runSuite(suite) {
    // suite执行开始
    this.emit(constants.EVENT_SUITE_BEGIN, suite);

    // 1. 执行before钩子函数
    if (suite._beforeAll.length) {
      for (const fn of suite._beforeAll) {
        const result = await fn();
        if (result instanceof Error) {
          this.emit(constants.EVENT_FAIL, `"before all" hook in ${suite.title}: ${result.message}`);
          // suite执行结束
          this.emit(constants.EVENT_SUITE_END);
          return;
        }
      }
    }
  
    // ...
  
    // 4. 执行after钩子函数
    if (suite._afterAll.length) {
      for (const fn of suite._afterAll) {
        const result = await fn();
        if (result instanceof Error) {
          this.emit(constants.EVENT_FAIL, `"after all" hook in ${suite.title}: ${result.message}`);
          // suite执行结束
          this.emit(constants.EVENT_SUITE_END);
          return;
        }
      }
    }
    // suite结束
    this.emit(constants.EVENT_SUITE_END);
  }
  
  /*
* 处理Test
*/
  async runTest(test) {
    // 1. 由suite根节点向当前suite节点,依次执行beforeEach钩子函数
    const _beforeEach = [].concat(this.suites).reverse().reduce((list, suite) => list.concat(suite._beforeEach), []);
    if (_beforeEach.length) {
      for (const fn of _beforeEach) {
        const result = await fn();
        if (result instanceof Error) {
          return this.emit(constants.EVENT_FAIL, `"before each" hook for ${test.title}: ${result.message}`)
        }
      }
    }
  
    // 2. 执行测试用例
    const result = await test.fn();
    if (result instanceof Error) {
      return this.emit(constants.EVENT_FAIL, `${test.title}`);
    } else {
      this.emit(constants.EVENT_PASS, `${test.title}`);
    }
 
    // 3. 由当前suite节点向suite根节点,依次执行afterEach钩子函数
    const _afterEach = [].concat(this.suites).reduce((list, suite) => list.concat(suite._afterEach), []);
    if (_afterEach.length) {
      for (const fn of _afterEach) {
        const result = await fn();
        if (result instanceof Error) {
          return this.emit(constants.EVENT_FAIL, `"after each" hook for ${test.title}: ${result.message}`)
        }
      }
    }
  }
}

Runner.constants = constants;
module.exports = Runner

在测试结果的处理函数中监听执行结果的回调进行统一处理:

// mocha/reporter/sped.js
const constants = require('../src/runner').constants;
const colors = {
  pass: 90,
  fail: 31,
  green: 32,
}
function color(type, str) {
  return '\u001b[' + colors[type] + 'm' + str + '\u001b[0m';
}
module.exports = function (runner) {
  let indents = 0;
  let passes = 0;
  let failures = 0;
  let time = +new Date();
  function indent(i = 0) {
    return Array(indents + i).join('  ');
  }
  // 执行开始
  runner.on(constants.EVENT_RUN_BEGIN, function() {});
  // suite执行开始
  runner.on(constants.EVENT_SUITE_BEGIN, function(suite) {
    ++indents;
    console.log(indent(), suite.title);
  });
  // suite执行结束
  runner.on(constants.EVENT_SUITE_END, function() {
    --indents;
    if (indents == 1) console.log();
  });
  // 用例通过
  runner.on(constants.EVENT_PASS, function(title) {
    passes++;
    const fmt = indent(1) + color('green', '  ✓') + color('pass', ' %s');
    console.log(fmt, title);
  });
  // 用例失败
  runner.on(constants.EVENT_FAIL, function(title) {
    failures++;
    const fmt = indent(1) + color('fail', '  × %s');
    console.log(fmt, title);
  });
  // 执行结束
  runner.once(constants.EVENT_RUN_END, function() {
    console.log(color('green', '  %d passing'), passes, color('pass', `(${Date.now() - time}ms)`));
    console.log(color('fail', '  %d failing'), failures);
  });
}

上面代码的作用对代码进行了收集。

验证
  • 截止到目前我们实现的mocha已经完成,执行下npm test看下用例的执行结果。

1ce322de4c872ed6f8bee41ceb0fca6c.png

我们再手动构造一个失败用例:

const assert = require('assert');
const { getTag } = require('../index');
describe('检查:getTag函数执行', function () {
  before(function() {
    console.log('😁before钩子触发');
  });
  describe('测试:正常流', function() {
    it('类型返回: [object JSON]', function (done) {
      setTimeout(() => {
        assert.equal(getTag(JSON), '[object JSON]');
        done();
      }, 1000);
    });
    it('类型返回: [object Number]', function() {
      assert.equal(getTag(1), '[object Number]');
    });
  });
  describe('测试:异常流', function() {
    it('类型返回: [object Undefined]', function() {
      assert.equal(getTag(undefined), '[object Undefined]');
    });
    it('类型返回: [object Object]', function() {
      assert.equal(getTag([]), '[object Object]');
    });
  });
  after(function() {
    console.log('😭after钩子触发');
  });
});

执行下:

56d285674aa9f9461d595ed560ffc784.png

一个精简版mocha就此完成!

后记

  • 整个mocha的核心思想还是十分简单的,但mocha的强大远不止此,mocha是个非常灵活的测试框架,可扩展性很高,但也与此同时会带来一些学习成本。像Jest那种包揽一切,断言库,快照测试,数据mock,测试覆盖率报告的生成等等全部打包提供的使用起来是很方便,但问题在于不方便去做一些定制化开发。而mocha搭配他的生态(用chai断言,用sinon来mock数据,istanbul来生成覆盖率报告等)可以很方便的去做一些定制化开发。

参考

https://github.com/mochajs/mocha

https://mochajs.org/

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

0a210f2c855babb367a34fb1f1bb4155.png


Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐