【问题标题】:Test-driven development of JavaScript web frontendsJavaScript Web 前端的测试驱动开发
【发布时间】:2011-04-01 14:02:43
【问题描述】:

这听起来可能有点愚蠢,但实际上我有点困惑如何处理 Web 前端的 JavaScript 测试。就我而言,典型的 3 层架构如下所示:

  1. 数据库层
  2. 应用层
  3. 客户端层

1 在这个问题上无关紧要。 2 包含所有程序逻辑(“业务逻辑”) 3 前端。

我为大多数项目进行测试驱动开发,但只针对应用程序逻辑,而不是前端。这是因为在 TDD 中测试 UI 是困难且不寻常的,通常不会进行。相反,所有应用程序逻辑都与 UI 分离,因此测试该逻辑很简单。

三层架构支持这一点:我可以将后端设计为由前端调用的 REST API。 JS 测试如何适应?对于典型的三层架构,JS(即客户端上的JS)测试没有多大意义,不是吗?

更新: 我已将问题的措辞从“在 Web 前端测试 JavaScript”更改为“JavaScript Web 前端的测试驱动开发”以澄清我的问题。

【问题讨论】:

    标签: javascript testing tdd


    【解决方案1】:

    请记住单元测试的意义:确保特定代码模块以预期的方式对某些刺激做出反应。在 JS 中,大部分代码(除非你有像 Sencha 或 YUI 这样的生命周期框架)要么直接操作 DOM,要么进行远程调用。要测试这些东西,您只需应用依赖注入和模拟/存根的传统单元测试技术。这意味着您必须编写要进行单元测试的每个函数或类,以接受依赖结构的模拟。

    jQuery 通过允许您将 XML 文档传递给所有遍历函数来支持这一点。而你通常会写

    $(function() { $('.bright').css('color','yellow'); }
    

    你会想写

    function processBright(scope) {
        // jQuery will do the following line automatically, but for sake of clarity:
        scope = scope || window.document;
    
        $('.bright',scope).css('color','yellow');
    }
    
    $(processBright);
    

    请注意,我们不仅从匿名函数中提取逻辑并为其命名,还使该函数接受范围参数。当该值为 null 时,jQuery 调用仍将正常运行。但是,我们现在有一个用于注入模拟文档的向量,我们可以在调用函数后对其进行检查。单元测试可能看起来像

    function shouldSetColorYellowIfClassBright() {
        // arrange
        var testDoc = 
            $('<html><body><span id="a" class="bright">test</span></body></html>');
    
        // act
        processBright(testDoc);
    
        // assert
        if (testDoc.find('#a').css('color') != 'bright')
            throw TestFailed("Color property was not changed correctly.");
    }
    

    TestFailed 可能如下所示:

    function TestFailed(message) {
        this.message = message;
        this.name = "TestFailed";
    }
    

    远程调用的情况与此类似,但与其实际注入一些设施,您可以使用掩码存根逃脱。假设你有这个功能:

    function makeRemoteCall(data, callback) {
        if (data.property == 'ok') 
            $.getJSON({url:'/someResource.json',callback:callback});
    }
    

    你可以这样测试它:

    // test suite setup
    var getJSON = $.getJSON;
    var stubCalls = [];
    $.getJSON = function(args) {
        stubCalls[stubCalls.length] = args.url;
    }
    
    // unit test 1
    function shouldMakeRemoteCallWithOkProperty() {
        // arrange
        var arg = { property: 'ok' };
    
        // act
        makeRemoteCall(arg);
    
        // assert
        if (stubCalls.length != 1 || stubCalls[0] != '/someResource.json')
            throw TestFailed("someResource.json was not requested once and only once.");
    }
    
    // unit test 2
    function shouldNotMakeRemoteCallWithoutOkProperty() {
        // arrange
        var arg = { property: 'foobar' };
    
        // act
        makeRemoteCall(arg);
    
        // assert
        if (stubCalls.length != 0)
            throw TestFailed(stubCalls[0] + " was called unexpectedly.");
    }
    
    // test suite teardown
    $.getJSON = getJSON;
    

    (您可以将整个内容包装在 module pattern 中,以免在全局命名空间中乱扔垃圾。)

    要以测试驱动的方式应用所有这些,您只需先编写这些测试。这是一种直接、简洁、最重要的是对 JS 进行单元测试的有效方法。

    像 qUnit 这样的框架可以用来驱动你的单元测试,但这只是问题的一小部分。您的代码必须以测试友好的方式编写。此外,Selenium、HtmlUnit、jsTestDriver 或 Watir/N 等框架用于集成测试,而不是用于单元测试本身。最后,您的代码决不能是面向对象的。单元测试的原理很容易与单元测试在面向对象系统中的实际应用相混淆。它们是独立但兼容的想法。

    测试风格

    我应该注意到,这里展示了两种不同的测试风格。第一个假设完全不了解 processBright 的实现。它可以使用 jQuery 添加颜色样式,也可以进行原生 DOM 操作。我只是在测试函数的外部行为是否符合预期。其次,我假设知道函数的内部依赖项(即 $.getJSON),并且这些测试涵盖与该依赖项的正确交互

    您采用的方法取决于您的测试理念和总体优先级以及您所处情况的成本效益概况。第一个测试比较纯。第二个测试简单但相对脆弱;如果我更改 makeRemoteCall 的实现,测试将中断。最好,makeRemoteCall 使用 $.getJSON 的假设至少由 makeRemoteCall 的文档证明是合理的。有几种更严格的方法,但一种经济高效的方法是将依赖项包装在包装函数中。代码库将仅依赖于这些包装器,它们的实现可以在测试时轻松地替换为测试存根。

    【讨论】:

    • 这很好!多亏了你,我以后一定会避免一些 WTFs :) 关于进一步阅读的任何提示?
    • 哎呀,编辑了我的第二个示例,以便它实际调用被测函数。嗯...就阅读而言,我想说只是阅读关于各种主题(不仅仅是 js,还包括测试和软件设计)的优秀博客(例如 john resig)和杂志(更好的软件杂志),并且只是同样重要的是:阅读/学习/理解其他人的代码。也不要害怕实际的语言规范:他们会带你从专业人士到大师。此外,《单元测试的艺术》是一本关于该主题的好书。
    • 此外,Mozilla 开发者中心(呃,现在称为 Mozilla 开发者网络)关于 javascript 的部分也非常宝贵。 developer.mozilla.org/en/javascript
    • 不错的答案!尽管如此,假设 jQuery 正确地进行远程调用和 DOM 修改,这归结为测试用户界面,而我通常不会这样做。但我可以看到它在 JS 中的效果比在富客户端 UI 上更好。
    【解决方案2】:

    Christian Johansen 写的一本书Test-Driven JavaScript Development可能会对你有所帮助。我只看过书中的一些示例(前几天刚刚下载了一个示例到 Kindle),但它看起来像是一本解决这个问题的好书。你可以去看看。

    (注:我和克里斯蒂安·约翰森没有任何关系,也没有对这本书的销售进行投资。看起来是解决这个问题的好东西。)

    【讨论】:

    • 不要吹毛求疵,但问题并没有要求参考书籍。您有本书中的示例或见解要分享吗?
    【解决方案3】:

    我有一个与 JS 客户端层相似的架构应用程序。就我而言,我使用我们公司自己的 JS 框架来实现客户端层。

    这个 JS 框架以 OOP 风格创建,因此我可以对核心类和组件进行单元测试。此外,为了涵盖所有用户交互(使用单元测试无法涵盖),我使用Selenium WebDriver 对框架可视组件进行集成测试,并在不同的浏览器下对其进行测试。

    因此,如果被测代码以 OOP 方式编写,TDD 可以应用于 JavaScript 开发。也可以进行集成测试(并且可以用来做某种 TDD)。

    【讨论】:

      【解决方案4】:

      还可以查看 QUnit,了解 JavaScript 方法和函数的单元测试。

      【讨论】:

      • 我见过的一种方法是 jquery 前端 + 一个安静的 Web 服务器。还有其他库,例如 BackBone.js。在这种情况下,QUnit 摇摆不定,您将您的 ui 代码分解为一组您也可以从测试框架调用的类/函数,然后您也将它们绑定到 GUI。我建议做一些实验
      【解决方案5】:

      您可以使用Rational Functional TesterHP tools 或其他等效软件等工具从用户角度测试您的应用程序。

      这些工具对应用程序进行测试,就像用户坐在它前面一样,但以自动化的方式进行。这意味着您可以同时测试所有三层,尤其是可能难以测试的 Javascript。像这样的功能测试可能有助于发现 UI 错误和 UI 如何使用中间层推出的数据的怪癖。

      不幸的是,这些工具非常昂贵,所以可能还有其他等价物(我很想知道这些工具)。

      【讨论】:

      • 我会说 TDD 被驱动到单元测试,而自动化工具则专注于集成/回归测试。如果我没有弄错问题,futlib 想知道是否有任何方法可以将 TDD 应用到 JavaScript 中,或者这样的 TDD 是否有意义。
      • Tiago,你把它钉牢了,稍微改写了这个问题以澄清这一点。我很抱歉模棱两可。
      【解决方案6】:

      在我们公司,我们使用jsTestDriver。这是一个用于测试前端的feature rich 环境。

      看看吧。

      【讨论】:

        猜你喜欢
        • 2017-05-10
        • 2011-09-09
        • 2012-10-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-26
        • 1970-01-01
        • 2010-11-21
        相关资源
        最近更新 更多