资讯专栏INFORMATION COLUMN

如何使用 mocha 和 sinon 集成单元测试--单元测试示例及分析(上)

Caicloud / 1020人阅读

摘要:使用集成单元测试上项目地址安装依赖测试框架可视化报表覆盖率替换依赖断言命令命令命令执行单元测试,并打开测试报告页面和覆盖率页面执行生成单元测试覆盖率并打开执行单个单元测试文

使用 mocha 集成单元测试(上)
项目地址:https://github.com/Jay-tian/j...
安装依赖
yarn add jquery mocha  mochawesome  istanbul  sinon chai jsdom decache babel-cli babel-core babel-preset-es2015 babel-plugin-module-resolver babel-istanbul  

mocha:测试框架
mochawesome:可视化报表
istanbul:覆盖率
sinon:替换依赖
chai:断言

scripts 命令

命令

  "scripts": {
    "test": "mocha --timeout 5000 --recursive --reporter mochawesome --require babel-core/register tests/src && open mochawesome-report/mochawesome.html && npm run test:cover",
    "test:cover": "babel-node ./node_modules/.bin/babel-istanbul cover _mocha -- tests/src/* -R spec --recursive && open coverage/lcov-report/index.html",
    "test:s": "mocha --recursive --require babel-core/register  --timeout 5000"
  }

test 命令:执行单元测试,并打开测试报告页面和覆盖率页面
test:cover 执行生成单元测试覆盖率并打开
test:s 执行单个单元测试文件

参数解析

--timeout 5000 超时设置
--recursive 包含子目录
--reporter mochawesome 通过mochawesome生成报表
--require babel-core/register 通过babel转译es6语法
tests/src 单元测试目录路径
open mochawesome-report/mochawesome.html 打开页面

测试含有jQuery的代码 初始化Jquery环境
let { JSDOM } = require("jsdom");
let dom = new JSDOM(``,{
    url: "http://127.0.0.1",
    referrer: "http://127.0.0.1",
    contentType: "text/html",
    userAgent: "Mellblomenator/9000",
    includeNodeLocations: true,
});
global.window = dom.window;
global.$ = require("jquery");
测试click事件
const { demo1 } = require("../../src/demo1.js");
const assert = require("chai").assert;
describe("demo1", function() {
  it("jquery click test", function() {
    demo1($("body"));
    assert.equal($("body").hasClass("hide"), false);
    $("body").trigger("click");
    assert.equal($("body").hasClass("hide"), true);
  });
});
运行结果

以上测试了,点击元素时,给该元素添加一个‘hide’类的方法
模拟jquery环境和触发click事件

测试post事件

由于初始化jquery环境比较通用,我们把它放到工具类去引用

utils.js
const decache = require("decache");
let { JSDOM } = require("jsdom");
exports.initJquery = function(html, params = {}){
    params = Object.assign({
        url: "http://127.0.0.1",
        referrer: "http://127.0.0.1",
        contentType: "text/html",
        userAgent: "Mellblomenator/9000",
        includeNodeLocations: true,
    }, params);
    let dom = new JSDOM(`${html}`, params);

    global.window = dom.window;
    decache("jquery");
    global.$ = require("jquery");
}

因为node环境中,require会有缓存,导致不同的单元测试间的初始环境不一致,需要手动清除缓存

 decache("jquery");
test.demo2.js
import post from "../../src/demo2.js"; 
const utils = require("./../utils");
const sinon = require("sinon");
require("./../utils");
describe("demo2", function() {
    before(function() {
        utils.initJquery("");
    });
    
    it("jquery post", function() {
        let stubPost = sinon.stub($, "post");
        let expectedUrl = "/demo2";
        let expectedParams = {"a": "abc"};
        post();
        sinon.assert.calledWith(stubPost, expectedUrl, expectedParams);
        stubPost.restore();
    });
});

restore()操作 将会复原被替换的对象
mocha 有四个钩子方法
before 在所有的单元测试运行前运行一次
after 在所有的单元测试运行结束运行一次
beforeEach 在每一个的单元测试运行前运行一次
afterEach 在每一个的单元测试运行后运行一次

测试ajax demo3.js
export default function() {
    $.ajax({
    type: "GET",
    url: null,
    async: true,
    promise: true,
    dataType: "json",
    beforeSend(request) {
    }
  });
}
test.demo3.js
import ajax from "../../src/demo3.js"; 
const utils = require("./../utils");
const sinon = require("sinon");
require("./../utils");
describe("demo3", function() {
    before(function() {
        utils.initJquery("");
    });
    
    it("jquery ajax", function() {
        let stubAjax = sinon.stub($, "ajax");
        let expectedParams = {
            type: "GET",
            url: null,
            async: true,
            promise: true,
            dataType: "json"
        };
        ajax();
        sinon.assert.calledWithMatch(stubAjax, expectedParams);
        stubAjax.restore();
    });
});

这里我们使用calledWithMatch断言参数,该方法可以断言传入的参数是否正确,不需要传入所有的参数

测试异步代码 demoe4.js
export default function() { 
    $("#demo4").hide();
    setTimeout(
        function(){
            $("#demo4").show();
        }, 1000);
}
import demo4 from "../../src/demo4.js"; 
const utils = require("./../utils");
const sinon = require("sinon");
const assert = require("chai").assert;
require("./../utils");
describe("asynchronous code", function() {
    let clock;
    before(function () { 
        utils.initJquery("
"); }); it("test by setTimeout", function(done) { let $demo = $("#demo4"); demo4(); assert.equal($demo.css("display"), "none"); let test = function() { assert.equal($demo.css("display"), "block"); // 这里的done告知这个单元测试结束了,保证不影响其他单元测试 done(); }; setTimeout(test, 1001); }); it("test by sinon", function() { //当利用了useFakeTimers后,事件将会停止 clock = sinon.useFakeTimers(); let $demo = $("#demo4"); //运行demo4前,元素还是显示的 assert.equal($demo.css("display"), "block"); demo4(); //运行demo4完,元素隐藏了 assert.equal($demo.css("display"), "none"); //时间穿梭101ms秒,定时器代码还未执行,所以元素还是隐藏的 clock.tick(101); assert.equal($demo.css("display"), "none"); //时间再穿梭900ms秒,就到达了1001ms后,定时器代码执行了,所以元素现在显示了 clock.tick(900); assert.equal($demo.css("display"), "block"); //恢复时间 clock.restore(); }); });

第一个单元测试利用了 setTimeout 去测试异步代码
第二个单元测试利用了 sinon 的时空穿梭器去测试异步代码
结果如图所示

第一个单元测试花了1035ms
而第二个单元测试几乎没有花费多少时间

所以异步代码编写单元测试时,第二个单元测试写法更优

需要测试的代码包含其他负责业务逻辑时 demo5.js
const demo5require = require("./demo5.require.js");

export default function() { 
    if(demo5require.a() == "a") {
        return 1;
    } else {
        return 2;
    }
}
test.demo5.js
import demo5 from "../../src/demo5.js"; 
const utils = require("./../utils");
const sinon = require("sinon");
const assert = require("chai").assert;

describe("demo5", function() {
    before(function () { 
        utils.initJquery("");
    });
    it("test", function() {
        assert.equal(demo5(), 1);
        const demo5require = require("../../src/demo5.require.js");
        let stub = sinon.stub(demo5require, "a").returns("b");
        assert.equal(demo5(), 2);
        stub.restore();
    });
});

此时demo5依赖其他模块,我们就可以替换demo5require的方法,并指定返回值,这样就不用关系依赖的模块做了什么业务。
测试结束,复原被替换的对象

webpack环境编写单元测试

webpack中会有设置别名的情况,这样单元测试有可能引入的模块的路径有误,这里我们可以使用babel-plugin-module-resolver进行别名的替换

.babelrc
{
  "presets": ["es2015"],
  "plugins": [
    ["module-resolver", {
      "root": ["./"],
      "alias": {
         "common":""
      }
    }]
  ]
}
运行结果

执行命令

npm run test

如图


最佳实践总结

文件名以及路径的定义如下,这样定义规范了路径的书写,便于文件的查找

/a/b/c/demo.js //待测试文件
/tests/a/b/c/test.demo.js //单元测试文件

一个单元测试文件测试一个js文件

一个describe测试一个方法

一个it 测试一个方法中一个逻辑,这样保证一个测试只验证一个行为

使用sinon隔离外部调用

使用before或beforeEach 初始环境

使用after或afterEach 清空或还原环境,不同单元测试互不影响,状态不共享

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93807.html

相关文章

  • 【Node Hero】9. Node.js 单元测试

    摘要:基本上,测试金字塔描述你应该编写单元测试集成测试和端到端测试。集成测试要比端到端测试多,单元测试甚至要更多一些。应用程序单元测试编写单元测试,是为了看看给定的模块单元是否工作。 本文转载自:众成翻译译者:网络埋伏纪事链接:http://www.zcfy.cc/article/1754原文:https://blog.risingstack.com/node-hero-node-js-un...

    104828720 评论0 收藏0
  • mocha、chai、sinonistanbul实现100%单元测试覆盖率

    摘要:加上测试覆盖率检查,就能够提供足够的信息,来断言代码的行为是否符合期望。测试的相关技术是程序的代码覆盖率工具,以土耳其最大城市伊斯坦布尔命名。 showImg(https://segmentfault.com/img/remote/1460000010260434); 敏捷软件开发中,最重要实践的就是测试驱动开发,在单元测试层面,我们试着实现一个重要的指标就是测试覆盖率。测试覆盖率衡量...

    Yuanf 评论0 收藏0
  • 前端单元测试初探

    摘要:本文只讨论单测的范畴,对集成测试有兴趣的话,可以看下的集成测试代码。前端单测现状测试本质上就是假定一个输入,然后判断得到预期的输出。 原文发于我的博客:https://github.com/hwen/blogS... 要不要写单测? 关于这个 cnode 上就有个很有意思的讨论 做个调查,你的 Node 应用有写单测吗? 看完这个应该会有结论?如果没有,就回帖跟别人探讨下~ 测试 测试...

    isLishude 评论0 收藏0
  • 使用karma+mocha+chai+sinon+@vue/test-utils为你的组件库增加单元

    摘要:但是,项目中的一些公共封装,比如公共的组件公用的功能模块等是可以使用单元测试的。因此特为组件库引入单元测试,目的在于能减少组件的,避免重复的发布不必要的包。 项目github地址:https://github.com/yuanalina/installAsRequired这里必须要提前说明,前端项目的单元测试不是必须的,特别是业务型项目,增加单元测试反而会成为累赘,增加开发成本且无意义...

    happen 评论0 收藏0
  • Ajax单元测试傻瓜教程

    摘要:原文出处单元测试傻瓜教程请求经常容易发生错误,客户端发送的数据出问题,服务器端返回的数据有误都会导致请求错误。设置在我们开始单元测试之前,我们需要安装几个必须的工具。我们将用它来向你们展示如何对进行单元测试。 原文出处 :AJAX单元测试傻瓜教程 Ajax 请求经常容易发生错误,客户端发送的数据出问题,服务器端返回的数据有误都会导致 Ajax 请求错误。你不能保证与服务器的连接总是工作...

    30e8336b8229 评论0 收藏0

发表评论

0条评论

Caicloud

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<