深入理解 Mocha 测试框架:从零实现一个 Mocha

发表于 2年以前  | 总阅读数:356 次

本文为来自飞书 aPaaS Growth 研发团队成员的文章。

aPaaS Growth 团队专注在用户可感知的、宏观的 aPaaS 应用的搭建流程,及租户、应用治理等产品路径,致力于打造 aPaaS 平台流畅的 “应用交付” 流程和体验,完善应用构建相关的生态,加强应用搭建的便捷性和可靠性,提升应用的整体性能,从而助力 aPaaS 的用户增长,与基础团队一起推进 aPaaS 在企业内外部的落地与提效。

前言

什么是自动化测试

  • 自动化测试在很多团队中都是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运行该测试用例的执行结果:

如上图所示,即我们前面测试代码的执行结果,我们来拆分下当前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]');
    });
  });
});

针对这段代码结构如下:

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 . 收集测试用例的执行结果。

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

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树创建出来是这样的结构:

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来实现。此时我们的流程图更新如下:

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中的回调函数进行处理,测试用例执行结束。但到这里我们会发现,只是执行了测试用例而已,测试用例的执行结果还没获取到,测试用例哪个通过了,哪个没通过我们也无法得知。

收集测试用例执行结果

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

修改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看下用例的执行结果。

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

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钩子触发');
  });
});

执行下:

一个精简版mocha就此完成!

后记

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

参考

https://github.com/mochajs/mocha

https://mochajs.org/

参考资料

[1]Mocha - the fun, simple, flexible JavaScript test framework: https://mochajs.org/

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/tpaCOMlVW8kV--s5SflvwQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录