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

news2024/12/27 0:24:38

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

        单元测试强调的是“独立性”,验证这个单元自身是否能正常工作。测试用例尽量不要依赖外部条件,必须依赖的外部条件不具备时自己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);
  • 3、测试用例中的外部依赖自己显式引用。
  • 4、测试前如何等待你的集成环境运行起来。  

        假设我们写了一个函数库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、测试框架如何处理公共变量

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

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;

2. 定义为局部变量,用eval函数执行测试用例代码;

3. 使用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沙箱中执行,这里要特别注意沙箱内外的细微差异。  

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

• 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);
  });
}

        总结:qunit、mocha可以用工作环境作集成测试,jest因为运行在vm沙箱中,直接用工作环境作集成测试时要特别注意沙箱带来的的微小差异。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2252292.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA 15 ,Xilinx Vivado 的基本使用,使用 Vivado 点亮LED灯,具体实现(使用赛灵思 Vivado编写 Verilog 代码)

目录 前言 我们都知道 Xilinx Vivado 是一款强大的 FPGA 集成开发环境&#xff0c;支持从设计输入、综合、实现到仿真的全流程。它兼容 Verilog 和 VHDL 等硬件描述语言&#xff0c;具备高层次综合功能&#xff0c;能有效提升复杂数字系统的设计效率和性能优化。这里来分享记…

VxLAN 集中式网关配置实验

一、拓扑&#xff1a; 二、配置思路&#xff1a; 1、CE1、2、3 配置 IGP&#xff0c;如 RIP 2、CE1、3 配置 BD 域并绑定二层 VNI&#xff0c;起用 NVE 并建立 VxLAN 隧道&#xff1a;源是自己的环回口&#xff0c;对端是 CE2、3 的环回口&#xff0c;用二层 VNI 联接&#x…

qml项目创建的区别

在Qt框架中&#xff0c;你可以使用不同的模板来创建应用程序。你提到的这几个项目类型主要针对的是Qt的不同模块和用户界面技术。下面我将分别解释这些项目类型的区别&#xff1a; 根据你提供的信息&#xff0c;以下是每个项目模板的详细描述和适用场景&#xff1a; Qt Widgets…

仿真s7 snap7 写入

# 准备写入数据 data bytearray(20) # 创建 20 字节的缓冲区 (每个整数占 2 字节&#xff0c;共 10 个整数)# 按顺序将值 1, 2, 3, ..., 10 写入数据缓冲区 for i in range(10):set_int(data, i * 2, i 1) # 每个整数占 2 字节&#xff0c;从 i * 2 的位置开始写入整数# 将…

【计算机网络】实验6:IPV4地址的构造超网及IP数据报

实验 6&#xff1a;IPV4地址的构造超网及IP数据报 一、 实验目的 加深对IPV4地址的构造超网&#xff08;无分类编制&#xff09;的了解。 加深对IP数据包的发送和转发流程的了解。 二、 实验环境 • Cisco Packet Tracer 模拟器 三、 实验内容 1、了解IPV4地址的构造超网…

[CSP-J 2024] 小木棍

题目传送门 P11229 [CSP-J 2024] 小木棍 题解思路&#xff1a; 1、首先想到的是搜索&#xff0c;全排列填盒子的思想&#xff0c;小木棍数够组成某个数&#xff0c;把某个数放到盒子里&#xff0c;这里就是累加到sum上&#xff0c;还做了个剪枝&#xff0c;中间如果已经大于…

AI运用落地思考:如何用AI进行物料条码的识别及异常检测?

一、AI进行物料条码识别 &#xff08;一&#xff09;基于深度学习的方法 图像预处理 首先需要对包含物料条码的图像进行预处理。这包括调整图像的大小、对比度、亮度等操作&#xff0c;以便提高条码图像的清晰度和可识别性。例如&#xff0c;如果图像较暗&#xff0c;可以通过…

基于STM32的电能监控系统设计:ModBus协议、RS-485存储和分析电能数据(代码示例)

一、项目概述 随着智能电网的发展&#xff0c;电能管理的科学性与有效性变得越来越重要。本项目旨在设计并实现一个基于STM32103C8T6单片机的电能监控系统&#xff0c;该系统可以实时采集、存储和分析电能数据&#xff0c;帮助用户实现对电能的高效管理。 项目目标 实时监控&…

从数据库模型设计到字段设计,用自然语言实现数据库开发,颠覆传统的数据库开发模式

前言 在数据库开发过程中&#xff0c;开发者经常面临以下困扰&#xff1a; 1. 焦头烂额的数据库设计阶段 在设计数据库阶段&#xff0c;开发者需要全面考虑表结构、关系模型、字段定义等&#xff0c;稍有不慎就会影响后续的开发与维护&#xff0c;常常让人感到无从下手。 2…

51c自动驾驶~合集39

我自己的原文哦~ https://blog.51cto.com/whaosoft/12707676 #DiffusionDrive 大幅超越所有SOTA&#xff01;地平线DiffusionDrive&#xff1a;生成式方案或将重塑端到端格局&#xff1f; 近年来&#xff0c;由于感知模型的性能持续进步&#xff0c;端到端自动驾驶受到了来…

docker安装hadoop环境

一、使用docker搭建基础镜像 1、拉取centos系统镜像 # 我这里使用centos7为例子 docker pull centos:7 2、创建一个dockerfiler文件&#xff0c;用来构建自定义一个有ssh功能的centos镜像 # 基础镜像 FROM centos:7 # 作者 #MAINTAINER hadoop ADD Centos-7.repo /etc/yum.re…

多级缓存设计实践

缓存是什么&#xff1f; 缓存技术是一种用于加速数据访问的优化策略。它通过将频繁访问的数据存储在高速存储介质&#xff08;如内存&#xff09;中&#xff0c;减少对慢速存储设备&#xff08;如硬盘或远程服务器&#xff09;的访问次数&#xff0c;从而提升系统的响应速度和…

状态模式S

状态模式&#xff08;State Pattern&#xff09;是行为设计模式的一种&#xff0c;它允许一个对象在其内部状态发生改变时改变其行为。这个对象被视为类型的有限状态机&#xff08;Finite State Machine&#xff09;。 在状态模式中&#xff0c;我们创建表示各种状态的对象和一…

数据结构 (23)并查集与等价类划分

一、并查集 并查集&#xff08;Union-Find Set或Disjoint Set&#xff09;是一种数据结构&#xff0c;用于处理一些不相交集合&#xff08;disjoint sets&#xff09;的合并及查询问题。它通常表示为森林&#xff0c;并用数组来实现&#xff08;类似于二叉堆&#xff09;。在并…

【Linux】开启你的Linux之旅:初学者指令指南

Linux相关知识点可以通过点击以下链接进行学习一起加油&#xff01; 在 Linux 开发中&#xff0c;GDB 调试器和 Git 版本控制工具是开发者必备的利器。GDB 帮助快速定位代码问题&#xff0c;Git 则提供高效的版本管理与协作支持。本指南将简明介绍两者的核心功能与使用技巧&…

Python语法1

Python语法1 作者&#xff1a;王珂 邮箱&#xff1a;49186456qq.com 文章目录 Python语法1[TOC] 前言一、环境搭建1.1 安装Python解释器1.2 安装第三方包1.3 安装Pycharm1.4 虚拟环境 二、Python语法2.1 基础语法2.1.1 注释2.1.2 变量2.1.3 数据类型2.1.4 关键字和标识符 2.2…

普及组集训--图论最短路径

定义&#xff1a;表示顶点u到顶点v的一条边的权值&#xff08;边权&#xff09; 最短路径算法有常见的四种&#xff1a;floyd&#xff0c;dijkstra&#xff0c;Bellman-Ford&#xff0c;SPFA 不过Bellman-Ford并不常用&#xff0c;所以本文不提&#xff1b; 重点在于dijkstr…

蓝桥杯第 23 场 小白入门赛

一、前言 好久没打蓝桥杯官网上的比赛了&#xff0c;回来感受一下&#xff0c;这难度区分度还是挺大的 二、题目总览 三、具体题目 3.1 1. 三体时间【算法赛】 思路 额...签到题 我的代码 // Problem: 1. 三体时间【算法赛】 // Contest: Lanqiao - 第 23 场 小白入门赛 …

前缀和篇——繁星斗斗数字交织中,觅得效率明月辉光(1)

前言 在这片无边无际的数字海洋中&#xff0c;如何从中提取出有价值的讯息&#xff0c;成为了计算机科学中的一项重要课题。前缀和算法&#xff0c;作为一种巧妙的技术&#xff0c;恰如其名——通过计算序列中各个元素的前缀和&#xff0c;能够为我们提供一种高效的查询方式&a…

STM32 HAL库 + LM2904运算放大器 + ADC + VDO温度传感器:电路设计及代码实现

本文将详细介绍如何使用STM32F407的HAL库&#xff0c;实现通过单通道ADC采集VDO温度传感器的信号&#xff0c;并通过串口将采集到的温度值打印输出。具体流程包括&#xff1a;通过分压电阻将获得VDO温度传感器的分压电压&#xff0c;再利用运算放大器LM2904对信号进行放大&…