高效 JavaScript 单元测试

fmms 13年前
     <p><strong>        一个损坏的 JavaScript 代码示例</strong></p>    <p>        Web 应用程序面临的一个最大挑战是支持不同版本的 Web 浏览器。能在 Safari 上运行的 JavaScript 代码不一定能在 Windows® Internet Explorer (IE)、Firefox 或 Google Chrome 上运行。这个挑战的根源是呈现层中的 JavaScript 代码从一开始就没有进行测试。如果没有对代码进行单元测试,那么在升级或支持新浏览器后,组织可能需要花钱反复测试 Web 应用程序。本文将展示如何通过高效的 JavaScript 代码单元测试降低测试成本。</p>    <p>        一个常见用例是登录表单 JavaScript 验证。考虑清单 1 中的表单。</p>    <p>        <strong>清单 1. 登录表单</strong></p>    <div class="cnblogs_code">     <pre class="brush:html; toolbar: true; auto-links: false;"><FORM>     <table>         <tr>             <td>Username</td>             <td><input type="text" id="username"/></td>             <td><span id="usernameMessage"></span></td>         </tr>         <tr>             <td>Password</td>             <td><input type="password" id="password"/></td>             <td><span id="passwordMessage"></span></td>         </tr>             <tr>             <td><input type="button" onclick="new appnamespace.             ApplicationUtil () .validateLoginForm ()" value="Submit"/></td>         </tr>     </table> </FORM></pre>    </div>    <p>        这个表单很简单,仅包含用户名和密码字段。单击提交按钮时,将通过 <code>ApplicationUtil</code> 执行一个特定的表单验证。以下是负责验证 HTML 表单的 JavaScript 对象。清单 2 显示了 <code>ApplicationUtil</code> 对象的代码。</p>    <p>        <strong>清单 2. 损坏的 ApplicationUtil 对象代码</strong></p>    <pre class="brush:javascript; toolbar: true; auto-links: false;">appnamespace = {};  appnamespace.ApplicationUtil = function() {};  appnamespace.ApplicationUtil.prototype.validateLoginForm =  function(){     var error = true;     document.getElementById ("usernameMessage") .innerText = "";     document.getElementById ("passwordMessage") .innerText = "";          if (!document.getElementById ("username") .value) {         document.getElementById ("usernameMessage") .innerText =          "This field is required";         error = false;     }          if (!document.getElementById ("password") .value) {         document.getElementById ("passwordMessage") .innerText =          "This field is required";         error = false;     }              return error;         };</pre>    <p>        在清单 2 中,<code>ApplicationUtil</code> 对象提供一个简单验证:用户名和密码字段都已填充。如果某个字段为空,就会显示一条错误消息:<code>This field is required</code>。</p>    <p>        上面的代码能够在 Internet Explorer 8 和 Safari 5.1 上工作,但无法在 Firefox 3.6 上工作,原因是 Firefox 不支持 <code>innerText</code> 属性。通常,(上述代码和其他类似 JavaScript 代码中的)主要问题是不容易发现编写的 JavaScript 代码是不是跨浏览器兼容的。</p>    <p>        这个问题的一个解决方案是进行自动化单元测试,检查代码是不是跨浏览器兼容。</p>    <p><strong>        JsTestDriver</strong></p>    <p>        JsTestDriver library 是最好的 JavaScript 单元测试框架之一,它为 JavaScript 代码提供了跨浏览器测试。图 1 展示了 JsTestDriver 的架构。</p>    <p>        <strong>图 1. JsTestDriver 架构</strong></p>    <p><img style="display:block;margin-left:auto;margin-right:auto;" alt="高效 JavaScript 单元测试" src="https://simg.open-open.com/show/42168d46d058ce6f438ea2d21c19a7f0.gif" width="411" height="325" /></p>    <p>        捕获不同的浏览器之后,服务器会负责将 JavaScript 测试用例运行程序代码加载到浏览器中。可以通过命令行捕获浏览器,也可以通过将浏览器指向服务器 URL 来捕获浏览器。一旦捕获到浏览器,该浏览器就被称为从属浏览器。服务器可以加载 JavaScript 代码,在每个浏览器上执行测试用例,然后将结果返回给客户端。</p>    <p>        客户端(命令行)需要以下两个主要项目:</p>    <ol>     <li>JavaScript 文件,即源文件和测试文件</li>     <li>配置文件,用于组织源文件和测试文件的加载</li>    </ol>    <p>        这个架构比较灵活,允许单个服务器从网络中的其他机器捕获任意数量的浏览器。例如,如果您的代码在 Linux 上运行但您想针对另一个 Windows 机器上的 Microsoft Internet Explorer 运行您的测试用例,那么这个架构很有用。</p>    <p>        要使用 JsTestDriver 库,请先下载最新版的 <a href="/misc/goto?guid=4959221801290737041">JsTestDriver 1.3.2</a>。</p>    <blockquote>     <strong>jsTestDriver 是开源项目</strong>     <p>jsTestDriver 是 <a href="/misc/goto?guid=4959221801369469361" target="_blank">Apache 2.0 许可</a> 下的一个开源项目,托管在 Google Code 上,后者是一个类似于 SourceForge 的项目存储库。只要使用 Open Source Initiative 批准的 <a href="/misc/goto?guid=4958328007566430815" target="_blank">许可</a>,开发人员就能在这个存储库中创建和管理公共项目。</p>     <p>还有许多其他 JavaScript 单元测试工具,请参见下面的 <a href="/misc/goto?guid=4959221801599844839" target="_blank">参考资料</a> 部分中的其他工具,比如 Dojo Objective Harness (DOH)。</p>    </blockquote>    <p>        编写单元测试代码</p>    <p>        现在开始编写 JavaScript 测试用例。为简单起见,我将测试以下用例:</p>    <ul>     <li>用户名和密码字段均为空。</li>     <li>用户名为空,密码不为空。</li>     <li>用户名不为空,密码为空。</li>    </ul>    <p>        清单 3 显示了表示 TestCase 对象的 <code>ApplicationUtilTest</code> 对象的部分代码。</p>    <p>        <strong>清单 3. ApplicationUtilTest 对象代码的一部分</strong></p>    <pre class="brush:javascript; toolbar: true; auto-links: false;"><strong>ApplicationUtilTest = TestCase ("ApplicationUtilTest");  ApplicationUtilTest.prototype.setUp = function () { /*:DOC += <FORM action=""><table><tr><td>Username</td><td> <input type="text" id="username"/></td><td><span id="usernameMessage"> </span></td></tr><tr><td>Password</td><td> <input type="password" id="password"/></td><td><span id="passwordMessage" ></span></td></tr></table></FORM>*/ };  ApplicationUtilTest.prototype.testValidateLoginFormBothEmpty = function () {     var applicationUtil = new appnamespace.ApplicationUtil ();          /* Simulate empty user name and password */     document.getElementById ("username") .value = "";     document.getElementById ("password") .value = "";         applicationUtil.validateLoginForm ();     assertEquals ("Username is not validated correctly!", "This field is required",      document.getElementById ("usernameMessage") .innerHTML);     assertEquals ("Password is not validated correctly!", "This field is required",      document.getElementById ("passwordMessage") .innerHTML);     };</strong></pre>    <p></p>    <p><code>        ApplicationUtilTest</code> 对象通过 JsTestDriver <code>TestCase</code> 对象创建。如果您熟悉 JUnit 框架,那么您肯定熟悉 <code>setUp</code> 和 <code>testXXX</code> 方法。<code>setUp</code> 方法用于初始化测试用例。对于本例,我使用该方法来声明一个 HTML 片段,该片段将用于其他测试用例方法。</p>    <p><code>        DOC</code> 注释是一个 JsTestDriver 惯用语,可以用于轻松声明一个 HTML 片段。</p>    <p>        在 <code>testValidateLoginFormBothEmpty</code> 方法中,创建了一个 <code>ApplicationUtil</code> 对象,并在测试用例方法中使用该对象。然后,代码通过检索用户名和密码的 DOM 元素并将它们的值设置为空值来模拟输入空用户名和密码。可以调用 <code>validateLoginForm</code> 方法来执行实际表单验证。最后,将调用 <code>assertEquals</code> 来确保 <code>usernameMessage</code> 和 <code>passwordMessage</code> span 元素中的消息是正确的,即:<code>This field is required</code>。</p>    <p>        在 JsTestDriver 中,可以使用以下构件:</p>    <ul>     <li><code>fail ("msg")</code>:表明测试一定会失败,消息参数将显示为一条错误消息。</li>     <li><code>assertTrue ("msg", actual)</code>:断定实际参数正确。否则,消息参数将显示为一条错误消息。</li>     <li><code>assertFalse ("msg", actual)</code>:断定实际参数错误。否则,消息参数将显示为一条错误消息。</li>     <li><code>assertSame ("msg", expected, actual)</code>:断定实际参数与预期参数相同。否则,消息参数将显示为一条错误消息。</li>     <li><code>assertNotSame ("msg", expected, actual)</code>:断定实际参数与预期参数不相同。否则,消息参数将显示为一条错误消息。</li>     <li><code>assertNull ("msg", actual)</code>:断定参数为空。否则,消息参数将显示为一条错误消息。</li>     <li><code>assertNotNull ("msg", actual)</code>:断定实际参数不为空。否则,消息参数将显示为一条错误消息。</li>    </ul>    <p>        其他方法的代码包含其他测试用例。清单 4 显示了测试用例对象的完整代码。</p>    <p>        <strong>清单 4. ApplicationUtil 对象完整代码</strong></p>    <pre class="brush:javascript; toolbar: true; auto-links: false;">ApplicationUtilTest = TestCase ("ApplicationUtilTest");  ApplicationUtilTest.prototype.setUp = function () { /*:DOC += <FORM action=""><table><tr><td>Username</td><td> <input type="text" id="username"/></td><td><span id="usernameMessage"> </span></td></tr><tr><td>Password</td><td> <input type="password" id="password"/></td><td><span id="passwordMessage" ></span></td></tr></table></FORM>*/ };  ApplicationUtilTest.prototype.testValidateLoginFormBothEmpty = function () {     var applicationUtil = new appnamespace.ApplicationUtil ();          /* Simulate empty user name and password */     document.getElementById ("username") .value = "";     document.getElementById ("password") .value = "";              applicationUtil.validateLoginForm ();          assertEquals ("Username is not validated correctly!", "This field is required",      document.getElementById ("usernameMessage") .innerHTML);     assertEquals ("Password is not validated correctly!", "This field is required",      document.getElementById ("passwordMessage") .innerHTML);     };  ApplicationUtilTest.prototype.testValidateLoginFormWithEmptyUserName = function () {     var applicationUtil = new appnamespace.ApplicationUtil ();          /* Simulate empty user name and password */     document.getElementById ("username") .value = "";     document.getElementById ("password") .value = "anyPassword";              applicationUtil.validateLoginForm ();          assertEquals ("Username is not validated correctly!",      "This field is required", document.getElementById ("usernameMessage") .innerHTML);     assertEquals ("Password is not validated correctly!",      "", document.getElementById ("passwordMessage") .innerHTML);     };  ApplicationUtilTest.prototype.testValidateLoginFormWithEmptyPassword = function () {     var applicationUtil = new appnamespace.ApplicationUtil ();          document.getElementById ("username") .value = "anyUserName";     document.getElementById ("password") .value = "";              applicationUtil.validateLoginForm ();          assertEquals ("Username is not validated correctly!",      "", document.getElementById ("usernameMessage") .innerHTML);     assertEquals ("Password is not validated correctly!",      "This field is required", document.getElementById ("passwordMessage").     innerHTML);     };</pre>    <p>    <strong>    配置用于测试的不同浏览器</strong></p>    <p>        测试 JavaScript 代码的一个推荐实践是将 JavaScript 源代码和测试代码放置在不同的文件夹中。对于图 2 中的示例,我将 JavaScript 源文件夹命名为 "js-src",将 JavaScript 测试文件夹命名为 "js-test",它们都位于 "js" 父文件夹下。</p>    <p>        <strong>图 2. JavaScript 测试文件夹结构</strong></p>    <p><img style="display:block;margin-left:auto;margin-right:auto;" alt="高效 JavaScript 单元测试" src="https://simg.open-open.com/show/f71cafa3f5838e6af9e78126d2eace00.jpg" width="291" height="402" /></p>    <p>        组织好源和测试文件夹后,必须提供配置文件。默认情况下,<code>JsTestDriver</code> 运行程序会寻找名为 jsTestDriver.conf 的配置文件。您可以从命令行更改配置文件名称。清单 5 显示了 <code>JsTestDriver</code> 配置文件的内容。</p>    <p>        <strong>清单 5. JsTestDriver 配置文件内容</strong></p>    <div class="cnblogs_code">     <pre>server: http:<span style="color:#008000;">//</span><span style="color:#008000;">localhost:9876</span><span style="color:#008000;"> </span> load:   - js-src<span style="color:#008000;">/*</span><span style="color:#008000;">.js   - js-test/*.js</span></pre>    </div>    <p>        配置文件采用 YAML 格式。<code>server</code> 指令指定测试服务器的地址,<code>load</code> 指令指出了将哪些 JavaScript 文件加载到浏览器中以及加载它们的顺序。</p>    <p>        现在,我们将在 IE、Firefox 和 Safari 浏览器上运行测试用例类。</p>    <p>        要运行测试用例类,需要启动服务器。您可以使用以下命令行启动 <code>JsTestDriver</code> 服务器:</p>    <div class="cnblogs_code">     <pre>java -jar JsTestDriver-1.3.2.jar --port 9876 --browser "[Firefox Path]",           "[IE Path]","[Safari Path]"</pre>    </div>    <p>        使用这个命令行,服务器将以 Port 9876 启动,捕获您的机器上的 Firefox、IE 和 Safari 浏览器。</p>    <p>        启动并捕获浏览器后,可以通过以下命令行运行测试用例类:</p>    <div class="cnblogs_code">     <pre>java -jar JsTestDriver-1.3.2.jar --tests all</pre>    </div>    <p>        运行命令后,您将看到第一轮结果,如清单 6 所示。</p>    <p>        <strong>清单 6. 第一轮结果</strong></p>    <div class="cnblogs_code">     <pre>Total 9 tests (Passed: 6; Fails: 3; Errors: 0) (16.00 ms)   Firefox 3.6.18 Windows: Run 3 tests (Passed: 0; Fails: 3; Errors 0) (8.00 ms)     ApplicationUtilTest.testValidateLoginFormBothEmpty failed (3.00 ms):      AssertError: Username is not validated correctly! expected "This field      is required" but was "" Error ("Username is not validated correctly!      expected \"This field is required\" but was \"\"")@:0()@http:<span style="color:#008000;">//</span><span style="color:#008000;">localhost</span><span style="color:#008000;"> </span>    :9876/test/js-test/TestApplicationUtil.js:16      ApplicationUtilTest.testValidateLoginFormWithEmptyUserName failed (3.00 ms):      AssertError: Username is not validated correctly! expected "This field is      required" but was "" Error ("Username is not validated correctly! expected      \"This field is required\" but was \"\"")@:0()@http:<span style="color:#008000;">//</span><span style="color:#008000;">localhost:9876/test</span><span style="color:#008000;"> </span>    /js-test/TestApplicationUtil.js:29      ApplicationUtilTest.testValidateLoginFormWithEmptyPassword failed (2.00 ms):      AssertError: Password is not validated correctly! expected "This field is      required" but was "" Error ("Password is not validated correctly! expected      \"This field is required\" but was \"\"")@:0()@http:<span style="color:#008000;">//</span><span style="color:#008000;">localhost:9876/test/</span><span style="color:#008000;"> </span>    js-test/TestApplicationUtil.js:42        Safari 534.50 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (2.00 ms)   Microsoft Internet Explorer 8.0 Windows: Run 3 tests (Passed: 3; Fails: 0;    Errors 0) (16.00 ms) Tests failed: Tests failed. See log <span style="color:#0000ff;">for</span> details.</pre>    </div>    <p>        注意,在清单 6 中,主要问题出在 Firefox 上。测试在 Internet Explorer 和 Safari 上均可顺利运行。<strong> <br /> </strong></p>    <p><strong>        修复 JavaScript 代码并重新运行测试用例</strong></p>    <p>        我们来修复损坏的 JavaScript 代码。我们将使用 <code>innerHTML</code> 替代 <code>innerText</code>。清单 7 显示了修复后的 <code>ApplicationUtil</code> 对象代码。</p>    <p>        <strong>清单 7. 修复后的 ApplicationUtil 对象代码</strong></p>    <div class="cnblogs_code">     <pre>appnamespace = {};  appnamespace.ApplicationUtil = <span style="color:#0000ff;">function</span>() {};  appnamespace.ApplicationUtil.prototype.validateLoginForm =  <span style="color:#0000ff;">function</span>(){     <span style="color:#0000ff;">var</span> error = <span style="color:#0000ff;">true</span>;     document.getElementById ("usernameMessage") .innerHTML = "";     document.getElementById ("passwordMessage") .innerHTML = "";          <span style="color:#0000ff;">if</span> (!document.getElementById ("username") .value) {         document.getElementById ("usernameMessage") .innerHTML =          "This field is required";         error = <span style="color:#0000ff;">false</span>;     }          <span style="color:#0000ff;">if</span> (!document.getElementById ("password") .value) {         document.getElementById ("passwordMessage") .innerHTML =          "This field is required";         error = <span style="color:#0000ff;">false</span>;     }              <span style="color:#0000ff;">return</span> error;         };</pre>    </div>    <p>        使用 <code>--test all</code> 命令行参数重新运行测试用例对象。清单 8 显示了第二轮运行结果。</p>    <p>        <strong>清单 8. 第二轮运行结果</strong></p>    <div class="cnblogs_code">     <pre>Total 9 tests (Passed: 9; Fails: 0; Errors: 0) (9.00 ms)   Firefox 3.6.18 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (9.00 ms)   Safari 534.50 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (5.00 ms)   Microsoft Internet Explorer 8.0 Windows: Run 3 tests (Passed: 3; Fails: 0;  Errors 0)    (0.00 ms)</pre>    </div>    <p><strong>        如清单 8 所示,JavaScript 代码现在在 IE、Firefox 和 Safari 上都能正常运行。</strong></p>    <p><strong>        结束语</strong></p>    <p>        在本文中,您了解了如何使用一个最强大的 JavaScript 单元测试工具 (JsTestDriver) 在不同的浏览器上测试 JavaScript 应用程序代码。还了解了什么是 JsTestDriver,如何配置它,以及如何在 Web 应用程序中使用它来确保应用程序的 JavaScript 代码的质量和可靠性。</p>    <p>        <strong>下载</strong></p>    <table style="width:100%;" class="ke-zeroborder" border="0" cellspacing="0" summary="This table contains downloads for this document." cellpadding="0">     <tbody>      <tr>       <th scope="col">描述</th>       <th scope="col">名字</th>       <th scope="col">大小</th>       <th scope="col">下载方法</th>      </tr>      <tr>       <td scope="row">源代码</td>       <td nowrap="">simple.zip</td>       <td nowrap="">3. 35MB</td>       <td nowrap=""><a href="http://www.ibm.com/developerworks/apps/download/index.jsp?contentid=777136&filename=simple.zip&method=http&locale=zh_CN">HTTP</a></td>      </tr>     </tbody>    </table>    <p>        <a href="/misc/goto?guid=4959221801853331933">关于下载方法的信息</a></p>    <p>        <strong>参考资料</strong></p>    <p>        <strong>学习</strong></p>    <ul>     <li>访问 <a href="/misc/goto?guid=4958195118394165382">JUnit.org</a>,了解如何使用 JUnit 测试框架。</li>     <li>详细了解 <a href="/misc/goto?guid=4958331407038500806">YAML</a>,这是一个针对所有编程语言的人类友好的数据序列化标准。</li>     <li>访问 developerWorks <a href="/misc/goto?guid=4958344115989055723">Open source 专区</a>获得丰富的 how-to 信息、工具和项目更新以及<a href="/misc/goto?guid=4958344116783737104">最受欢迎的文章和教程</a>,帮助您用开放源码技术进行开发,并将它们与 IBM 产品结合使用。</li>    </ul>    <p>        <strong>获得产品和技术</strong></p>    <ul>     <li>如果您对应用程序测试感兴趣,请试用我们的 <a href="/misc/goto?guid=4959221802281772305">IBM Rational Functional Tester</a> 试用软件。</li>     <li>访问 <a href="/misc/goto?guid=4959221802366027432">JsTestDriver 下载页</a>,获取最新代码、插件等资源。</li>     <li>访问 dojo <a href="/misc/goto?guid=4959221802455318653">项目网站</a>,了解另一个单元测试工具 Dojo Objective Harness (DOH)。</li>     <li>探索 <a href="/misc/goto?guid=4959221802529577111">JavaScript 单元测试框架</a> 的范围。</li>    </ul>    <p>        <strong>讨论</strong></p>    <ul>     <li>帮助构建 developerWorks 社区中的 <a href="/misc/goto?guid=4958327760228428355">真实世界开源</a> 讨论组。</li>     <li>加入 <a href="/misc/goto?guid=4958344118369454520">developerWorks 中文社区</a>,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。</li>     <li>加入 <a href="/misc/goto?guid=4958344119172756503">IBM 软件下载与技术交流群组</a>,参与在线交流。</li>    </ul>    <p>        <strong>关于作者</strong></p>    <p>        Hazem Saleh 有 6 年的 JEE 和开源技术经验。他致力于 Apache MyFaces 方面的工作,是 MyFaces 项目许多组件的发起人,比如 Tomahawk CAPTCHA、Commons ExportActionListener、Media、PasswordStrength 等等。他是 GMaps4JSF(一个集成 Google Maps 和 Java ServerFaces 的集成项目)和 Mashups4JSF(集成 mashup 服务和 JavaServer Faces 的集成项目)的创始人,是《The Definitive Guide to Apache MyFaces and Facelets (Apress)》和其他许多 JSF 文章的作者,并且是 developerworks 的投稿人和 JSF 演讲家。他现在是 IBM Egypt 的资深软件工程师和 Web 2.0 技术的主题专家。<br /> <br /> 来自: <a id="link_source2" href="/misc/goto?guid=4959221802775257033" target="_blank">www.ibm.com</a></p>