0.0.3 • Published 3 years ago

node-tmf v0.0.3

Weekly downloads
1
License
MIT
Repository
github
Last release
3 years ago

node-tmf

全称 node test module function, 指用于在 Node 端对模块导出的函数做单元测试。

为什么需要

单元测试过程中,一般会 mock 掉不能正常运行的函数,比如数据库操作函数、网络请求函数。于此,常用的工具库有 sinon. 以下为一段示例:

// user.js
const db = require('db');
exports.getUser = (userId) => {
  return db.queryUser(userId);
};

// user.test.js
const db = require('db');
const sinon = require('sinon');
const user = require('./user');

describe('user test', () => {
  it('#getUser()', () => {
    // mock 掉 db 对象中的 `getUser` 方法,使得在接下来的
    // 单元测试执行过程中真正的 `db.getUser` 不会被执行,
    // 这样就能保证即使没有数据库的支持,单元测试也能正常执行。
    const queryUser = sinon.stub(db, 'queryUser');
    const user = user.getUser(123);
    // 还原 mock
    queryUser.restore();

    // 确认`db.queryUser`确实被调用了
    assert.ok(queryUser.called);
    // 确认`db.queryUser`的入参是正确的
    assert.equal(queryUser.lastCallArguments, [123]);
  });
});

如上,我们一般是对对象的某个属性方法进行 mock, 如果某个方法恰巧就是模块默认导出函数,没有挂载的父对象,这种情况就难以处理。比如,简单修改以上的 user.js:

// user.js
const queryUser = require('queryUser');
exports.getUser = (userId) => {
  return queryUser(userId);
};

// 或者
const { queryUser } = require('db');
exports.getUser = (userId) => {
  return queryUser(userId);
};

如上,这两种情况似乎就难以 mock. node-tmf 在这两种情况能发挥较大做用,在示例中说明。

示例

// user.js
const queryUser = require('queryUser');
exports.getUser = (userId) => {
  return queryUser(userId);
};

// 或者
const { queryUser } = require('db');
exports.getUser = (userId) => {
  return queryUser(userId);
};

// user.test.js
const queryUser = require('queryUser');
// 或者如下,视具体情况而定。
const { queryUser } = require('db');

const { mock } = require('node-tmf');
const user = require('./user');

describe('user test', () => {
  it('#getUser()', () => {
    // 将指定的 `queryUser` mock 成你想要的函数,这样,原本真正对接数据库的
    // 函数将不会被执行。
    const mockQueryUser = mock(queryUser, () => ({ name: 'Tom' }));
    const user = user.getUser(123);
    // 还原 mock. 还原后,再次调用 `queryUser` 则又会执行真正的数据库调用。
    queryUser.restore();

    // 确认`queryUser`确实被调用了
    assert.ok(queryUser.called);
    // 确认`queryUser`的入参是正确的
    assert.equal(queryUser.lastCallArguments, [123]);
    // 确认 `queryUser` 的返回值符合预期。注意,这里的返回值是指 mock 后的
    // 函数的返回值
    assert.deepEqual(queryUser.lastReturnValue, { name: 'Tom' });
  });
});

安装

使用 npm

$ npm install node-tmf --save-dev

使用 yarn

$ yarn add node-tmf --dev

使用方法

以 mocha 为例,需要分两步:

第一步

在单元测试运行程序中添加 -r, --require 参数。

// package.json
{
  "scripts": {
    "test": "mocha -r node-tmf/lib/inject.js test/**/*.test.js"
  }
}

第二步

在单元测试代码中,引入 node-tmf 模块对目标方法机型 mock 或者 spy. 具体可参考示例。

API

mock

function(originFunc: Function, mockFunction?: Function): SpyInfo

mock 函数用于将原函数 originFunc mock 成指定的 mockFunction. 当 mockFunction 为空时,则会为期分配一个默认的空函数。空函数的类型自动保持与 originFunc 类型一致,即 Function, AsyncFunction, 或者 GeneratorFunction.

返回说明:mock 方法返回的 SpyInfo 记录着函数的调用情况,具体参数见 返回值SpyInfo说明

spy

function(targetFunc: Function): SpyInfo

spy 函数用于监听并收集目标函数 targetFunc 的执行信息,只是充当一个间谍函数。也就是说,原本的目标函数 targetFunc 逻辑仍然会被执行(mock 的则不会)。

返回说明:spy 方法返回的 SpyInfo 记录着函数的调用情况,具体参数见 返回值SpyInfo说明

返回值SpyInfo说明

{
  called: boolean,      // 函数是否被执行过
  callCount: number,    // 函数被执行次数
  callArgs: any[][],    // 按顺序记录函数每次执行时的入参
  lastCallArgs: any[],  // 函数最后一次执行时的入参
  returnValues: any[],  // 按照顺序记录函数每次执行的返回值
  lastReturnValue: any, // 函数最后一次执行的返回值
}

示例:

// math.js
exports.add = (a, b) => a + b;

// math.test.js
const tmf = require('node-tmf');
const { add } = require('./math');

describe('math', () => {
  it('#add()', () => {
    const spyAdd = tmf.spy(add);
    assert.equal(add(1, 2), 3);
    assert.equal(add(3, 4), 7);
    assert.ok(spyAdd.called);
    assert.equal(spyAdd.callCount, 2);
    assert.deepEqual(spyAdd.callArgs, [[1, 2], [3, 4]]);
    assert.equal(spyAdd.lastCallArgs, [3, 4]);
    assert.equal(spyAdd.returnValues, [3, 7]);
    assert.equal(spyAdd.lastReturnValue, 7);
  });
});

局限性

局限一

不能 mock 或 spy 异步挂载的方法。例如,以下模块的 add 能够被 mock 和 spy, 但 mul 方法则不能被 mock 或 spy.

exports.add = (a, b) => a + b;

setTimeout(() => {
  exports.sub = (a, b) => a - b;
});

局限二

使用 Object.defineProperty 方法并设置 configurable: false 的导出方法不能被 mock 或者 spy. 例如,以下模块的 add 能够被 mock 和 spy, 但 mul 方法则不能被 mock 或 spy.

exports.add = (a, b) => a + b;

Object.defineProperty(exports, 'sub', {
  value: ((a, b) => a - b),
  configurable: false, // configurable 默认为 false
});

局限三

node-tmf 会限制导出函数的层级深度,最多五层。例如,以下 funcInDepth5 方法能够被 mock 或 spy, funcInDepth6 方法则不能。

module.exports = {
  nest1: {
    nest2: {
      nest3: {
        nest4: {
          funcInDepth5: () => 5,
          nest5: {
            funcInDepth6: () => 6,
          },
        },
      },
    },
  },
};

不过,你可以通过设置 TMF_MAX_DEPTH 环境变量,自定义最大层级深度。以上,可以通过设置 TMF_MAX_DEPTH 环境变量为 6 解决问题。

原理说明

node-tmf 通过覆盖 Module.wrapper 重写了 require 方法,在每个 require 的模块中注入了一段代码,这段代码会递归遍历本模块导出方法,并为其包装一层 Proxy, 通过 Proxy 截获函数的调用,并依赖其实现 mockspy 方法。

结语

node-tmf 并不是一个完整的单元测试辅助库,它只是对一些极端情况的补充,实际情况中,一般是以 sinon 为主,node-tmf 为辅。对此,你可以把 node-tmf 当做是一个补丁,专门用于应付模块默认导出函数,和源码中以解构形式导入函数的极端情况。