Karma+mocha+webpack 自动化单测实践

对于web开发者而言,单元测试是项目产品可靠性的重要保障。一个工程项目库的质量是否优秀,通过其在测试方面所做的工作就可以充分衡量。由于前端插件库落地场景繁杂,所面向的既有node环境,也有web环境(甚至将来可能还要考虑electron等),因此需要测试框架具备多情景测试的能力。

然而,多数测试框架并不是为业务场景所设计,这就需要我们通过一个hub来将不同功能定位的测试元件糅合到一起,这个hub便是Angular团队所开源的Karma

一、什么是好的单测框架

经过几天的爬坑,以及对前端数据缓存库的单测编写实践,笔者认为一套完整的单测框架系统应当具备以下feature:

1. 自动化测试:支持script执行及file watch,可以集成到git hooks或grunt/gulp工作流中
2. web环境:适应基于webAPI研发的插件测试需求,也可提供文档流供dom组件测试,测试出现问题时,允许开发者在真实浏览器环境中调试,支持source-map
3. ES6语法支持:支持源码引入,npm包引入,测试用例编写体验的基本保障,捕捉异步代码异常时尤为明显(有关此方案下对ts的支持可参考本文Ref)
4. 本地代理&Mock:网络传输层相关测试必备
5. 代码覆盖率报告:衡量测试质量的重要标准,同时也是case编写者的重要参考

二、如何搭建自动测试流

需要Cover上述所有需求的单测框架,其执行逻辑应该如下图所示:

1. 自动打包构建的支持是实现“ES6编辑,Web环境测试”的基础,笔者曾尝试jest+pupeteer的方案,通过page.evaluate传递es6代码,babel-polyfill提供运行时环境支持,然而pupeteer对这种形式的兼容欠佳,async function甚至需要以字符串形式传输才能确保不出问题,这对于测试用例编写者而言无疑是非常别扭的;

2. 浏览器环境的调起和console信息的捕获是整个自动化测试流程中最重要的部分,这也是笔者选择karma框架的原因,虽然依赖注入这种声明式的写法会使得配置文件越来越冗长,但是只要做好指定,这部分工作就可以由karma框架自动完成,工作流不会阻塞,调整起来也十分灵活。

理清整个流程后,我们就可以确认实现各个部分所需要的工具:

1. 代码改动触发:karma配置autoWatch = true, singleRun = false持续运行服务
2. Git提交触发:husky配置git hooks, 执行karma start --single-run,实现单次运行服务,失败时自动阻塞Git提交
3. 打包构建:webpack或rollup,(代码覆盖率统计方面,因rollup-istanbul-plugin存在代码覆盖标记定位不准确的问题,故只考虑webpack + istanbul-instrument-loader的方案,不考虑此需求可以使用rollup + babel的方案,karma同样支持)
4. 本地服务代理:karma自身集成,可注册中间件plugin实现mock
5. 调起浏览器环境:karma相关launcher包可以调起各家浏览器,这里推荐使用puppeteer(比phantomJS在api支持上更稳定)和karma-chrome-launcher来做,完全静默运行
6. 执行测试用例:mocha (测试框架), chai (断言库), chai-as-promised (异步断言扩展)
7. 捕获浏览器执行结果:karma自身集成,capture浏览器输出到终端
8. 生成测试报告:karma-mocha-reporter, karma-coverage-istanbul-reporter(输出代码覆盖率)

梳理之后,我们可以得到这样的一个package.json列表:

"chai": "^4.2.0", // BDD / TDD 风格断言库
"chai-as-promised": "^7.1.1", // chai异步断言扩展
"husky": "^3.0.9", // git钩子
"karma": "^4.4.1", // 测试运行环境
"karma-chai": "^0.1.0", // chai注入插件
"karma-chrome-launcher": "^3.1.0", // chrome浏览器调起插件
"karma-cli": "^2.0.0", // 命令行工具
"karma-coverage": "^2.0.1", // 代码覆盖率统计插件
"karma-mocha": "^1.3.0", // mocha注入插件
"karma-mocha-reporter": "^2.2.5", // mocha报告生成插件
"karma-webpack": "^4.0.2", // webpack预处理插件
"karma-coverage-istanbul-reporter": "^2.1.0", // istanbul代码覆盖率统计插件
"istanbul-instrumenter-loader": "^3.0.1", // istanbul计数webpack-loader
"mocha": "^6.2.2", // mocha测试框架,提供单测API
"puppeteer": "^1.20.0", // headless浏览器
"webpack": "^4.41.2" // 打包工具

看上去很多,那么把这些积木搭好后我们能得到一个怎样的效果呢?

首先,对ES6语法的支持使得我们编写测试用例时可以很方便的捕获webAPI的异步错误

// 引入promise支持
import chaiPromise from 'chai-as-promised';
chai.use(chaiPromise);
...          
    // localStorage失效异常检测
    it('EXCEPTION', async function() {
        await setDataAsync('storage', 1);
        const storage = indexedDB;
        // 删除idb对象,强制触发读取数据异常
        delete indexedDB;
        // 捕获promise异常,需注意应当捕获reject而不是exception
        await expect(getDataAsync('storage')).to.be.rejectedWith(Error);
        // 还原对象,获取到相关值
![60969619-Screen Shot 2016-09-13 at 2.24.26 pm.png](https://upload-images.jianshu.io/upload_images/19978227-2f7799d6ff5e2f44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
        indexedDB = storage;
        // 捕获promise resolve值
        await expect(getDataAsync('storage')).to.eventually.equal(1);
    });
...

接下来我们挂起测试服务,可以看到该用例正常得以执行:

通过这个执行log,我们可以看到karma实际的运行流程基本与预期的一致,它会首先调用webpack对测试文件及源码进行打包构建,然后建立起自身服务,并调用launcher插件打开puppeteer,与其建立起socket通信,之后将各测试用例的执行结果以mocha的格式打印到终端,测试完成后输出代码覆盖率统计信息。

此外,我们还可以进一步查看输出的icov格式报告,查看更详细的统计结果:

source-map的支持,使得我们可以直接在es6源码文件上进行分析测试未覆盖到的代码段,精确的定位可以有效的帮助测试开发者知晓真正的问题出在哪里,一般来说,多数branch覆盖率警告问题来源于冗余的兜底。

如果我们在开发时不慎删除了重要代码,导致无法覆盖到运行时异常,那么很快就可以在终端里看到测试未通过的报警:

得益于整个流程的自动化及合理的测试用例设计,现在这样的问题将永远不会出现在code review中。

三、karma runtime环境配置

在前一章中,我们介绍了自动测试框架的运行流程和相关依赖,下面这张图是对其整体的一个梳理:

理解这张图只需要理解以下3点:

  1. 中间的整条管线就是karma的运行流,从输入到输出,karma本质上只是一个测试运行环境server;
  2. processor, framework, browser, middleware这些模块是karma的配置项,karma主要通过注入的方式实现和测试框架、浏览器的接合;
  3. reporter的相关插件注入是需要和对应框架并行配置的,例如:istanbul的代码覆盖率检测需要在processors里通过webpack写入计数器并实现source-map,在reporter里通过karma-istanbul-coverage解析计数器得到代码覆盖率报告,并通过source-map映射到源码位置;

mocha的测试用例报告需要在framework和report里分别注入'mocha', 并引入karma-mocha-reporter插件,这样才能在命令行中看到简介的mocha测试报告

实际上我们需要做的只有4步:

  1. npm install需要的库,它们在前一章节已经给出
  2. 执行karma init,并选择对应的框架和浏览器,生成karma.conf.js文件
  3. 手动配置该文件,把我们在图中提到的各个部分一一引入:
const path = require('path');
// 让chromeHeadLess指向puppeteer
process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
    config.set({
        basePath: '',
        // 添加mocha测试框架,chai断言库
        frameworks: ['mocha', 'chai'],
        files: ['test/*.spec.js'],
        // 添加mock配置并通过plugins指向对应js文件
        middleware: ['mock'],
        plugins: ['karma-*', require('./test/mock/middleware')],
        // 打包构建
        preprocessors: {
            "test/*.spec.js": ['webpack'],
        },
        // webpack相关配置,无需指定entry, karma视测试文件为入口
        webpackMiddleware: {
            // 屏蔽webpack log信息
            noInfo: true,
            stats: 'errors-only'
        },
        webpack: {
            mode: 'development',
            // 启用source-map
            devtool: 'inline-source-map',
            module: {
                rules: [
                    {
                        test: /\.js?$/,
                        // 指定源码位置
                        include: [path.join(__dirname, 'src')],
                        enforce: 'post',
                        use: {
                            // 注入istanbul代码覆盖计数
                            loader: 'istanbul-instrumenter-loader',
                            options: { esModules: true }
                        }
                    }
                ],
            },
            resolve: {
                extensions: ['.js', '.json']
            }
        },
        // 添加相关报告插件
        reporters: ['mocha', 'coverage-istanbul'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        // 自动监视文件改动并触发测试
        autoWatch: true,
        // 调起chromeHeadless浏览器环境,使用真实浏览器环境调试需要指定为Chrome
        browsers: ['ChromeHeadless'],
        // 为true时执行一次后结束,false时持续运行,用于监视文件改动时触发测试,或在浏览器环境中进行调试
        singleRun: false,
        // 浏览器调起阈值
        concurrency: 6000,
        // istanbul代码覆盖率测试报告配置,主要为输出lcov格式报告
        coverageIstanbulReporter: {
            reports: ['html', 'lcovonly', 'text-summary'],
            // /%browser%/可实现调用不同浏览器测试时分别计算代码覆盖率
            dir: path.join(__dirname, 'test/coverage/%browser%/'),
            fixWebpackSourcePaths: true,
            'report-config': {
                html: { outdir: 'html' }
            }
        },
        // 屏蔽浏览器log,仅显示测试用例执行情况
        client: {
            captureConsole: false
        },
    })
}

四、本地Mock实现

karma自身提供了一个middleware的mock factory接口,可以通过返回一个function来处理req, res, next三个参数,因此实现mock只需:

  1. 解析req中的路由
  2. 根据req请求的内容,重定向的本地文件,通过fs.readfileSync读取
  3. 将读取数据注入到res中返回,如遇异常则调用next()交由下文代码处理

以笔者项目结构为例,之前配置文件中所require的./test/mock/middleware正是下图中的middleware.js:


对应的实现逻辑:

function mockFactory(config) {
    const mockUrl = config.mockUriStart || '/mock/';
    return function (req, res, next) {    
        //  检测Url匹配, 获取mock指向的路由
        if (req.url.indexOf(mockUrl) === 0) {
            const path = req.url.slice(mockUrl.length);
            try {
                const processor = require(`./processors/api`);
                processor(path, res);
            }
            catch (e) {
                next();
            }
        }
        else {
            next();
        }
    };
};
module.exports = {
    'middleware:mock': ['factory', mockFactory]
};

有关数据流的处理则收敛到processors/api.js的逻辑中:

module.exports = function (req, res) {
    const file_path = path.join(DATA_SRC, `${req}.json`);
    try {
        // 获取data文件中对应的json数据
        var data = fs.readFileSync(file_path);
        res.end(data);
    } catch (err) {
        // Handle a file-not-found error
        throw err;
    }
};

至此,整套测试框架的配置已经完成,可以在test目录下编写自己的.spec.js单测文件了~

当然,如果希望这个测试服务可以集成到git precommit钩子上,我们只需要在package.json中通过husky声明:

"husky": {
    "hooks": {
        // 预编译钩子仅执行一次全部测试
      "pre-commit": "karma start --signle-run"
    }
}

五、其他细节&踩坑记录

  1. mocha测试框架中, it函数内调用的异步函数出现断言错误,是不会引发测试报警的,必须要将断言语句直接放在it内:
async function postData(cb) {
    new Promise((resolve) => {
        setTimeout(() => {
            resolve('success');
        }, 1e3);
    }).then(cb);
}
...
    it('TEST ASYNC', async () => {
        await postData((resp) => {
            expect(resp).to.be.equal('fail') // 内层断言失败,不判定case失败
        });
        await expect(new Promise((resolve) => {
            postData((resp) => {
                resolve(resp);
            });
        }).to.eventually.equal('fail'); // 外层断言失败,判定case失败 
    })
...

  1. karma是多线程打印输出的,这意味着多个reporter同时运行时,log会重复打印在终端中,如果想capture client的console log,最好还是选择在browsers配置中选择调起Chrome浏览器测试,而不是使用ChromeHeadless:

    这里可以看到完整的log,热更新源码js或测试文件的时候需要手动刷新

  2. 如果使用rollup+babel,需要为babel添加transform-runtime插件并启用runtimeHelpers,babel应当如下配置,并注入到rollup的plugins中(preprocessor仅使用rollup)

{
    exclude: '**/node_modules/**',
    runtimeHelpers: true,
    plugins: [
        ['@babel/transform-runtime', { regenerator: true, useESModules: true }],
    ],
}
  1. 编写多个测试用例文件的情况下,请注意,所有的测试文件共享一个浏览器环境的上下文,这意味着测试用例中的代码可能会对全局变量造成污染,虽然karmas-iframe支持为每个测试用例建立单独的window上下文,但该插件对karma做了一些破坏性改动,使得代码覆盖率测试的结果会受到影响,因此只能予以放弃

  2. 如果希望修改某个测试用例时,不用再触发其他所有case的执行,可以通过给测试用例添加it.only或describe.only来实现,不过需注意此种情况下,对应的代码覆盖率也会只统计该本轮测试中覆盖的内容,所以最好仅在编辑测试用例时或调试使用时only声明,最终提交时去除,以免影响代码覆盖率统计及测试质量

  3. 使用safari浏览器测试会强制弹窗要求用户选择重定向文件,该过程会阻塞测试,目前暂无办法有效解决

Reference

[1] webpack+mocha+karma搭建typescript测试环境教程:https://blog.crimx.com/2019/06/19/%E6%90%AD%E5%BB%BA-karma-mocha-chai-%E6%B5%8B%E8%AF%95-typescript-%E9%A1%B9%E7%9B%AE/
[2] karma实现mock: https://github.com/renaesop/blog/issues/18
[3] Vue单元测试(mocha+chai)://www.greatytc.com/p/34afa645487b
[4] karma + phantomjs使用:https://juejin.im/post/59807358518825563e037e3c
[5] jest + puppeteer单元测试:https://juejin.im/post/5af90988518825426a1fcc2e
[6] karma + sauce lab多端云测:https://github.com/karma-runner/karma-sauce-launcher
[7] mocha + chai单测用例编写教程:http://www.alloyteam.com/2013/12/hour-class-learning-costs-javascript-unit-testing-tool-matcha-mocha-and-chai/
[8] How to test with async/wait using mocha&chai: https://stackoverflow.com/questions/45466040/verify-that-an-exception-is-thrown-using-mocha-chai-and-async-await
[9] 轻量级react技术栈单测框架(mocha+chai+nyc):https://ole.michelsen.dk/blog/testing-reactjs-with-coverage-using-mocha-babel-istanbul.html
[10] karma相关issues

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 前言 本篇文章是我在学习前端自动化单元测试时的一些思路整理,之前也从未接触过单元测试相关工具,如有错漏,请读者斧正...
    Awey阅读 12,623评论 8 38
  • Vue笔记七: Vue的自动化测试 前言 为什么我们需要测试? 让产品可以快速迭代,同时还能保持高质量 -- 阮一...
    brandonxiang阅读 15,990评论 2 10
  • 原文链接:http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutor...
    butterflyq阅读 1,987评论 1 3
  • 感恩,知足,微笑,简单, 我一直会这样, 保持这种轻松愉悦的心情, 去好好地度过每一天,每一年。 用最美的心情,过...
    张一朵阅读 181评论 0 1
  • 一→整理于2002年10月14日 2002年9下旬去了四川九寨沟和长江三峡旅行。感受良多,一直想...
    在天边在人间阅读 83评论 0 0