编写可测试的 JavaScript

简介:

Twitter 的工程师文化要求进行测试,许多的测试。在进入 Twitter 之前我还未有过测试 JavaScript 的经验,所以在这之后我学习到了很多。特别是学到了许多过去我使用、书写和鼓励使用的代码其实是不利于书写可测试的代码的。所以我觉得在此分享我所学习到有价值的,如何书写可测试的 JavaScript 几条最重要的原则。这里提供的这些示例虽然基于 QUnit,但是也应该适用于其他的 JavaScript 测试框架。

避免单例

我最受欢迎的博文中的其中一篇就是关于如何使用 JavaScript 模块模式 在程序中创建强大的单例。这种做法简单有效,但是给测试带来了问题。理由很简单: 单例在测试间造成了状态污染 。与其把单例当作模块使用,不如把他们写成可构造的对象。一旦应用程序初始化,就在全局层上分配一个单一的、默认的实例。

例如,考虑如下的单例模块(当然,是人为的例子):

var dataStore = (function() {
    var data = [];
    return {
        push: function (item) {
            data.push(item);
        },
        pop: function() {
            return data.pop();
        },
        length: function() {
            return data.length;
        }
    };
}());

有了这个模块,我们可能想测试 foo.bar 方法。以下是一个简单的 QUnit 测试套件:

module("dataStore");
test("pop", function() {
    dataStore.push("foo");
    dataStore.push("bar")
    equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item");
});

test("length", function() {
    dataStore.push("foo");
    equal(dataStore.length(), 1, "adding 1 item makes the length 1");
});

在运行测试套件时,length 断言会测试失败,但是这里很难弄清它为什么会失败。问题就在于上一次测试中 dataStore 的状态留了下来。如果只是给测试重新排序的话 length 测试会通过,但是会有红色标志标明某处出现了问题。我们当然可以使用 setup 或者 teardown 方法,用恢复 dataStore 的状态来修复此问题,但那也同时代表着我们需要在 dataStore 模块的实现改动了以后经常维护这样的测试模板。更好的做法如下:

function newDataStore() {
    var data = [];
    return {
        push: function (item) {
            data.push(item);
        },
        pop: function() {
            return data.pop();
        },
        length: function() {
            return data.length;
        }
    };
}
var dataStore = newDataStore();

现在,测试套件看起来如下:

module("dataStore");
test("pop", function() {
    var dataStore = newDataStore();
    dataStore.push("foo");
    dataStore.push("bar")
    equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item");
});

test("length", function() {
    var dataStore = newDataStore();
    dataStore.push("foo");
    equal(dataStore.length(), 1, "adding 1 item makes the length 1");
});

这让我们的全局 dataStore 和以前的行为保持一致,同时避免了测试之间的相互污染。每项测试都有自己的DataStore 实例对象,都会在测试完成时进入垃圾回收。

避免基于闭包的私有形式

我过去所推崇的另一个模式是 在 JavaScript 中建立真正的私有成员。这样做的好处是,可以保持全局可访问的命名空间免受不必要的,私有实现引用细节的侵扰。然而过度使用这种模式会导致代码无法测试。这是因为你的测试套件将无法访问到闭包中隐藏的私有函数,也就无法进行测试了。考虑以下的代码:

function Templater() {
    function supplant(str, params) {
        for (var prop in params) {
            str.split("{" + prop +"}").join(params[prop]);
        }
        return str;
    }

    var templates = {};

    this.defineTemplate = function(name, template) {
        templates[name] = template;
    };

    this.render = function(name, params) {
        if (typeof templates[name] !== "string") {
            throw "Template " + name + " not found!";
        }

        return supplant(templates[name], params);
    };
}

Templater 对象中的关键方法是 supplant,但是我们并不能从构造器闭包的外部访问到此方法。所以,与 QUnit 类似的测试套件并不能如我们期待的那般工作。另外,我们无法在不尝试调用 .render() 方法,让它作用于模板,查看所生成异常的情况下来验证 defineTemplate 方法的效果。我们当然可以简单地添加一个 getTemplate() 方法,并为了测试而把方法暴露为公有接口,但这并不是一件好的做法。在这个简单示例中这么做可能问题不大,但是在构建复杂对象的时候,如果使用了重要的私有方法,将会导致依赖不可测试的标红代码。这里是上面代码的可测试版本:

function Templater() {
    this._templates = {};
}

Templater.prototype = {
    _supplant: function(str, params) {
        for (var prop in params) {
            str.split("{" + prop +"}").join(params[prop]);
        }
        return str;
    },
    render: function(name, params) {
        if (typeof this._templates[name] !== "string") {
            throw "Template " + name + " not found!";
        }

        return this._supplant(this._templates[name], params);
    },
    defineTemplate: function(name, template) {
        this._templates[name] = template;
    }
};

这里是对应的 QUnit 测试套件:

module("Templater");
test("_supplant", function() {
    var templater = new Templater();
    equal(templater._supplant("{foo}", {foo: "bar"}), "bar"))
    equal(templater._supplant("foo {bar}", {bar: "baz"}), "foo baz"));
});

test("defineTemplate", function() {
    var templater = new Templater();
    templater.defineTemplate("foo", "{foo}");
    equal(template._templates.foo, "{foo}");
});

test("render", function() {
    var templater = new Templater();
    templater.defineTemplate("hello", "hello {world}!");
    equal(templater.render("hello", {world: "internet"}), "hello internet!");
});

注意代码中对 render 的测试仅仅是一个确保 defineTemplate 和 supplant 能够互相整合的测试。我们已经单独测试了这些方法,从而让我们可以很容易发现 render 的测试失败是具体哪个组件导致的。

编写紧密联系的多个函数

在任何语言中,紧密联系的函数都是重要的,JavaScript 也展示了这么做的原因。你使用 JavaScript 完成的大部分都是由环境提供的全局单例,也是测试套件所依赖的东西。例如,如果你的所有方法都在尝试给 window.location 赋值,那么测试 URL rewriter 就会有困难。与此相反,你应当将系统分解成对应的逻辑组件,决定它们如何去做,并编写实际完成的简短函数。你可以使用多个输入输出测试这些函数逻辑,而不测试那个修改 window.location 的最终函数。这么做既可以正确地组合系统,也能保证安全。

这里是不可测试的 URL rewriter 示例:

function redirectTo(url) {
    if (url.charAt(0) === "#") {
        window.location.hash = url;
    } else if (url.charAt(0) === "/") {
        window.location.pathname = url;
    } else {
        window.location.href = url;
    }
}

虽然示例中的逻辑很简单,但我们也能设想到情况更复杂的 redirecter 。随着复杂度的上升,我们不能在不触发 window 重定向的情况下测试这个方法,而这样会完全离开测试套件。

这里是可测试版本:

function _getRedirectPart(url) {
    if (url.charAt(0) === "#") {
        return "hash";
    } else if (url.charAt(0) === "/") {
        return "pathname";
    } else {
        return "href";
    }
}

function redirectTo(url) {
    window.location[_getRedirectPart(url)] = url;
}

而现在我们可以为 _getRedirectPart 编写一个简单的测试套件:

test("_getRedirectPart", function() {
    equal(_getRedirectPart("#foo"), "hash");
    equal(_getRedirectPart("/foo"), "pathname");
    equal(_getRedirectPart("http://foo.com"), "href");
});

现在最重要的 redirectTo 已经通过测试,我们就不必担心会意外地跳转到测试套件之外了。

注意:有一种备选解决方案是创建 performRedirect 函数做地址跳转,但是在测试套件中隔离此函数。这是许多人的常用实践,但是我会尽量避免方法隔离。我发现在我目前的所有情形中 QUnit 基本上工作得很好,并且更倾向于像上面那样,不用在测试中隔离函数,但是你的情形可能会不太一样。

编写大量测试

这是明摆着的事情,但是仍然要记住它。许多程序员写的测试太少,因为写测试很难,或者很费事。我一直都被这个问题所困扰,所以我写出了一个 QUnit 助手让写大量的测试更简单。这是一个叫 testCases 的函数,你在 test 块中可以调用,可以传进一个函数,调用上下文和输入/输出的数组用来尝试及比对。你可以为你的输入/输出函数快速地构建出健壮缜密的测试。

function testCases(fn, context, tests) {
    for (var i = 0; i < tests.length; i++) {
        same(fn.apply(context, tests[i][0]), tests[i][1],
            tests[i][2] || JSON.stringify(tests[i]));
    }
}

这里是一个简单的使用示例:

test("foo", function() {
    testCases(foo, null, [
        [["bar", "baz"], "barbaz"],
        [["bar", "bar"], "barbar", "a passing test"]
    ]);
});

总结

关于可测试的 JavaScript 有很多要写的内容。我确信这类优秀书籍有很多,但是我希望这篇文章能基于我的日常所得,提供一份实用案例的概览。因为我并不是一个测试专家,所以如果我出错了,或者提供了不好的建议,请告诉我。

文章转载自 开源中国社区[http://www.oschina.net]

相关文章
|
2月前
|
数据采集 人工智能 自然语言处理
Midscene.js:AI 驱动的 UI 自动化测试框架,支持自然语言交互,生成可视化报告
Midscene.js 是一款基于 AI 技术的 UI 自动化测试框架,通过自然语言交互简化测试流程,支持动作执行、数据查询和页面断言,提供可视化报告,适用于多种应用场景。
557 1
Midscene.js:AI 驱动的 UI 自动化测试框架,支持自然语言交互,生成可视化报告
|
5月前
|
Web App开发 JavaScript 前端开发
添加浮动按钮点击滚动到网页底部的纯JavaScript演示代码 IE9、11,Maxthon 1.6.7,Firefox30、31,360极速浏览器7.5.3.308下测试正常
添加浮动按钮点击滚动到网页底部的纯JavaScript演示代码 IE9、11,Maxthon 1.6.7,Firefox30、31,360极速浏览器7.5.3.308下测试正常
|
3月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
609 1
|
9月前
|
Web App开发 JavaScript 前端开发
《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)
【5月更文挑战第3天】本文介绍了如何在Web自动化测试中使用JavaScript执行器(JavascriptExecutor)来完成Selenium API无法处理的任务。首先,需要将WebDriver转换为JavascriptExecutor对象,然后通过executeScript方法执行JavaScript代码。示例用法包括设置JS代码字符串并调用executeScript。文章提供了两个实战场景:一是当时间插件限制输入时,用JS去除元素的readonly属性;二是处理需滚动才能显示的元素,利用JS滚动页面。还给出了一个滚动到底部的代码示例,并提供了详细步骤和解释。
129 10
|
4月前
|
人工智能 监控 JavaScript
模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域
模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域
52 1
|
4月前
|
JavaScript 前端开发
JavaScript - 测试 Prototype
JavaScript - 测试 Prototype
25 0
|
4月前
|
JavaScript 前端开发
JavaScript - 测试 jQuery
JavaScript - 测试 jQuery
30 0
|
6月前
|
Web App开发 应用服务中间件 定位技术
three.js:三维模型加载量测试
three.js:三维模型加载量测试
253 4
|
6月前
|
JavaScript 前端开发 测试技术
Vue.js开发者必看!Vue Test Utils携手端到端测试,打造无懈可击的应用体验,引领前端测试新风尚!
【8月更文挑战第30天】随着Vue.js的普及,构建可靠的Vue应用至关重要。测试不仅能确保应用质量,还能提升开发效率。Vue Test Utils作为官方测试库,方便进行单元测试,而结合端到端(E2E)测试,则能构建全面的测试体系,保障应用稳定性。本文将带你深入了解如何使用Vue Test Utils进行单元测试,通过具体示例展示如何测试组件行为;并通过Cypress进行E2E测试,确保整个应用流程的正确性。无论是单元测试还是E2E测试,都能显著提高Vue应用的质量,让你更加自信地交付高质量的应用。
103 0
|
6月前
|
JavaScript 前端开发 应用服务中间件
【qkl】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能
【区块链】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能
190 0

热门文章

最新文章

  • 1
    当面试官再问我JS闭包时,我能答出来的都在这里了。
    53
  • 2
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
    30
  • 3
    Node.js 中实现多任务下载的并发控制策略
    34
  • 4
    【2025优雅草开源计划进行中01】-针对web前端开发初学者使用-优雅草科技官网-纯静态页面html+css+JavaScript可直接下载使用-开源-首页为优雅草吴银满工程师原创-优雅草卓伊凡发布
    26
  • 5
    【JavaScript】深入理解 let、var 和 const
    49
  • 6
    【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
    48
  • 7
    【03】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架搭建-服务端-后台管理-整体搭建-优雅草卓伊凡商业项目实战
    59
  • 8
    【02】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-ui设计图figmaUI设计准备-figma汉化插件-mysql数据库设计-优雅草卓伊凡商业项目实战
    58
  • 9
    如何通过pm2以cluster模式多进程部署next.js(包括docker下的部署)
    72
  • 10
    【01】Java+若依+vue.js技术栈实现钱包积分管理系统项目-商业级电玩城积分系统商业项目实战-需求改为思维导图-设计数据库-确定基础架构和设计-优雅草卓伊凡商业项目实战
    58