专业编程基础技术教程

网站首页 > 基础教程 正文

qunit/mocha/jest在nodejs下的集成测试原理分析

ccvgpt 2024-12-23 09:05:16 基础教程 1 ℃

#jest##mocha##测试#

qunit、mocha、jest是javascript单元测试框架中比较流行的几个。

qunit/mocha/jest在nodejs下的集成测试原理分析

单元测试强调的是“独立性”,验证这个单元自身是否能正常工作。测试用例尽量不要依赖外部条件,必须依赖的外部条件不具备时自己mock模拟,不需要等待别的同事提供条件给你。

集成测试强调的是“协作”,在正式工作环境中,验证全部单元是否能彼此匹配的正常工作。

但在实践中,单元对外部的依赖是常常发生的,如果都要自己mock一个环境,工作量就比较大,因此想在集成的环境中来做单元测试。即,问题是:qunit、mocha、jest能做集成测试吗?

可能要关注的几个问题点:

  1. 因为集成测试环境(webserver)是在主进程运行,要求测试用例不能再开子进程运行。
  2. 测试用例不要在vm这种沙箱中运行,因为沙箱内外环境很难保持完全一致。写一个简单代码验证一下这种差异:
let vm=require('vm');
let ctx=vm.createContext({
  emptyArray:[],
  console
});
vm.runInContext(`
  console.log([] instanceof Array);//......true
  console.log(emptyArray instanceof Array);//......false
  `,ctx);
  1. 测试用例中的外部依赖自己显式引用。
  2. 测试前如何等待你的集成环境运行起来。

假设我们写了一个函数库yjUtils.js:

var yjUtils = {
  /**
   * 简单对象,普通对象,即:通过 "{}" 或者 "new Object" 创建的对象。有原型。
   * 不包括pure object
   * @param {*} value 
   * @returns 
   */
  isPlainObject(value) {
    if (!value) return false;
    if (typeof value !== 'object') return false;
    let proto = value;
    while (Object.getPrototypeOf(proto) !== null) {
      proto = Object.getPrototypeOf(proto);
    }
    return Object.getPrototypeOf(value) === proto;
  },
  /**
   * 纯粹对象,用Object.create(null)创建的对象,无原型。
   * @param {any} value 
   */
  isPureObject(value){
    if (!value) return false;
    return (typeof value==='object') && !Object.getPrototypeOf(value);
  },
  isObject(value){
    /**
     * class的typeof为'function'
     * 如果要包含pure object,用typeof value=='object'
     * 否则用value instanceof Object
     * 注意:Array.isArray(value)比value instanceof Array更可靠。
     * 注意:value instanceof Date不可靠:尽管 instanceof 可以很好地工作,但在某些情况下,Object.prototype.toString.call 更可靠。特别是在跨窗口或跨 iframe 的环境中,不同窗口的 Date 构造函数可能是不同的,这会导致 instanceof 失败。
     * 注意:value instanceof RegExp不可靠。
     */
    console.log('...yjUtils.js,inner [] is Array?',process.pid,[] instanceof Array);
    console.log('...yjUtils.js,outer [] is Array?',value,value instanceof Array);
    if (!value) return false;
    return (typeof value==='object') && 
      !(Array.isArray(value)) && 
      (Object.prototype.toString.call(value) !== '[object Date]') &&
      (Object.prototype.toString.call(value) !== '[object RegExp]');
  },
  /**
   * 是否是引用类型,如:object,array,function,class,regexp,date
   * @param {any} value 
   * @returns 
   */
  isRefType(value){
    if (!value) return false;
    return (typeof value==='object') || (typeof value==='function');
  },
  ......
}
module.exports = yjUtils;

我们用jest编写一个测试用例testcase_yjUtils.js:

//jest测试用例
function sum(a,b){}
class YJDemo{}
class YJDate extends Date{}
let sep='.........................................................................';
function getTestInfo(what,deal,toBe){
  let s=what;
  s=s+sep.substring(0,30-s.length);
  s=s+deal;
  s=s+sep.substring(0,60-s.length);
  s=s+toBe;
  return s;
}
let testFuncs=['isPureObject','isPlainObject','isObject','isRefType'];
let testCases=[
  ['new Object()'       ,new Object()       ,false,true ,true ,true],
  ['Object.create(null)',Object.create(null),true ,false,true ,true],
  ['{}'                 ,{}                 ,false,true ,true ,true],
  ['null'               ,null               ,false,false,false,false],
  ['undefined'          ,undefined          ,false,false,false,false],
  ['[]'                 ,[]                 ,false,false,false,true],
  ['array:[1,2,3]'      ,[1,2,3]            ,false,false,false,true],
  ['string:"abc"'       ,'abc'              ,false,false,false,false],
  ['string:""'          ,''                 ,false,false,false,false],
  ['number:189'         ,189                ,false,false,false,false],
  ['number:0'           ,0                  ,false,false,false,false],
  ['number:-1'          ,-1                 ,false,false,false,false],
  ['boolean:false'      ,false              ,false,false,false,false],
  ['boolean:true'       ,false              ,false,false,false,false],
  ['function:sum'       ,sum                ,false,false,false,true],
  ['class:YJDemo'       ,YJDemo             ,false,false,false,true],
  ['new YJDemo()'       ,new YJDemo()       ,false,false,true ,true],
  ['Object'             ,Object             ,false,false,false,true],
  ['String'             ,String             ,false,false,false,true],
  ['RegExp'             ,RegExp             ,false,false,false,true],
  ['RegExp:/ a/'        ,/ a/               ,false,false,false,true],
  ['Symbol'             ,Symbol             ,false,false,false,true],
  ['Symbol()'           ,Symbol()           ,false,false,false,false],
  ['new Date()'         ,new Date()         ,false,false,false,true],
  ['new YJDate()'       ,new YJDate()       ,false,false,false,true],
]

for(let i=0;i<testCases.length;i++){
  let testCase=testCases[i];
  describe(testCase[0],function(){
    for(let j=0;j<testFuncs.length;j++){
      let result=yjUtils[testFuncs[j]](testCase[1]);
      let toBe=testCase[2+j];
      let info=getTestInfo(testCase[0],testFuncs[j],toBe);
      test(info,function(){
        expect(result).toBe(toBe);
      })
    }
  });
}

qunit写法稍微不同:

//qunit测试用例
......
for(let i=0;i<testCases.length;i++){
  let testCase=testCases[i];
  QUnit.module(testCase[0]);
  for(let j=0;j<testFuncs.length;j++){
    let result=yjUtils[testFuncs[j]](testCase[1]);
    let toBe=testCase[2+j];
    let info=getTestInfo(testCase[0],testFuncs[j],toBe);
    QUnit.test(info,function(assert){
      assert.equal(result,toBe);
    })
  }
}

mocha写法又稍微不同:

//mocha测试用例
var assert = require("assert");
......
for(let i=0;i<testCases.length;i++){
  let testCase=testCases[i];
  describe(testCase[0],function(){
    for(let j=0;j<testFuncs.length;j++){
      let result=yjUtils[testFuncs[j]](testCase[1]);
      let toBe=testCase[2+j];
      let info=getTestInfo(testCase[0],testFuncs[j],toBe);
      it(info,function(){
        assert.equal(result,toBe);
      })
    }
  });
}

我们看到在测试用例testcase_yjUtils.js中,这里有2个特别注意的地方:

  1. 没有引用来源的隐式使用了测试框架的函数:describe、test、expect、QUnit、it;
  2. 隐式使用了yjUtils这个变量。测试用例中应该是自己解决yjUtils的引用?还是要依赖外部?单元测试的时候依赖外部(外部供给会有多种方法,如在浏览器中<script src=''/>方式引入;nodejs中require引入;mock模拟引入);集成测试的时候,最好自己显式引用。

测试框架如何处理公共变量

测试框架是如何让这些没有引用来源的函数正确执行的呢?不同的框架有不同的实现方法,可能的方式是:

  1. 挂在global上;
  • qunit v2.22.0,是在\qunit\src\cli\run.js中:
async function run (args, options) {
  ......
  QUnit = requireQUnit();
  ......
  // TODO: Enable mode where QUnit is not auto-injected, but other setup is
  // still done automatically.
  global.QUnit = QUnit;
  ......
  • mocha v10.8.2,是在\mocha\lib\interfaces\bdd.js中,context传入的就是global对象:
module.exports = function bddInterface(suite) {
  ......
  suite.on(EVENT_FILE_PRE_REQUIRE, function (context, file, mocha) {
    ......
    context.it = context.specify = function (title, fn) {
      var suite = suites[0];
      if (suite.isPending()) {
        fn = null;
      }
      var test = new Test(title, fn);
      test.file = file;
      suite.addTest(test);
      return test;
    };
  • jest v29.7.0,是在\jest\node_modules\jest-environment-node\build\index.js中:
class NodeEnvironment {
  ......
  constructor(config, _context) {
    const {projectConfig} = config;
    this.context = (0, _vm().createContext)();
    const global = (0, _vm().runInContext)(
      'this',
      Object.assign(this.context, projectConfig.testEnvironmentOptions)
    );
    this.global = global;
    const contextGlobals = new Set(Object.getOwnPropertyNames(global));
    for (const [nodeGlobalsKey, descriptor] of nodeGlobals) {
      if (!contextGlobals.has(nodeGlobalsKey)) {
        if (descriptor.configurable) {
          Object.defineProperty(global, nodeGlobalsKey, {
            configurable: true,
            enumerable: descriptor.enumerable,
            get() {
              const value = globalThis[nodeGlobalsKey];
              // override lazy getter
              Object.defineProperty(global, nodeGlobalsKey, {
                configurable: true,
                enumerable: descriptor.enumerable,
                value,
                writable: true
              });
              return value;
            },
            set(value) {
              // override lazy getter
              Object.defineProperty(global, nodeGlobalsKey, {
                configurable: true,
                enumerable: descriptor.enumerable,
                value,
                writable: true
              });
            }
          });
        } else if ('value' in descriptor) {
          Object.defineProperty(global, nodeGlobalsKey, {
            configurable: false,
            enumerable: descriptor.enumerable,
            value: descriptor.value,
            writable: descriptor.writable
          });
        } else {
          Object.defineProperty(global, nodeGlobalsKey, {
            configurable: false,
            enumerable: descriptor.enumerable,
            get: descriptor.get,
            set: descriptor.set
          });
        }
      }
    }
    // @ts-expect-error - Buffer and gc is "missing"
    global.global = global;
  1. 定义为局部变量,用eval函数执行测试用例代码;
  2. 使用vm建立沙箱;
  • jest v29.7.0把一些函数挂在了global后,同时用vm建立了沙箱,用runInContext执行代码,在\jest\node_modules\jest-runtime\build\index.js中:
class Runtime {
  ......
  _execModule(localModule, options, moduleRegistry, from, moduleName) {
    ......
    const transformedCode = this.transformFile(filename, options);
    let compiledFunction = null;
    const script = this.createScriptFromCode(transformedCode, filename);
    let runScript = null;
    const vmContext = this._environment.getVmContext();
    if (vmContext) {
      runScript = script.runInContext(vmContext, {
        filename
      });
    }
    if (runScript !== null) {
      compiledFunction = runScript[EVAL_RESULT_VARIABLE];
    }
    if (compiledFunction === null) {
      this._logFormattedReferenceError(
        'You are trying to `import` a file after the Jest environment has been torn down.'
      );
      process.exitCode = 1;
      return;
    }
    ......

从代码看,jest的测试用例没办法绕过vm沙箱,只能在vm沙箱中执行,这里要特别注意沙箱内外的细微差异。

如何在测试前启动你的集成环境

  • qunit v2.22.0,用命令执行:qunit ./foil-run.cjs
//foil-run.cjs
QUnit.begin(function(data){
  console.log('......qunit begin......',data);
  /**
   * 只有QUnit.begin会等待异步函数执行完毕
   */
  return new Promise(function(resolve){
    let foilStart=require('../_start.js');
    //启动foil webserver
    foilStart(function(){
      //foil-webserver启动完成后,开始执行测试用例
      //testCase_yjUtils.js内部自己显式引用yjUtils.js
      require('./testcase/testCase_yjUtils.js');
      resolve();
    });
  });
});
QUnit.done(function(data){
  process.exit();
});
  • mocha v10.8.2,用命令行执行:mocha foil-run.js --delay

注意:一定要加--delay参数。

//foil-run.js
let foilStart=require('../_start.js');
foilStart(function(){
  require('./testcase/testcase_yjUtils.js');
  run();
  after(function(){
    process.exit();
  });
});
  • jest v29.7.0,用命令行执行:jest
//jest.config.js
module.exports = {  
  maxWorkers:1,//设置为1,在主进程执行
  testMatch:['**/testcase/testcase_yjUtils.js'],
  globalSetup:'./foil-setup.js',
  globalTeardown:'./foil-teardown.js'
}
//foil-setup.js
module.exports = async function (globalConfig, projectConfig) {
  let start=require('../_start.js');
  await new Promise(function(resolve){
    start(resolve);
  });
}
//foil-teardown.js
module.exports = async function (globalConfig, projectConfig) {
  process.exit();
};

Tags:

最近发表
标签列表