编写可测试的Javascript代码(1):反模式及其解决方案
<!DOCTYPE html> <html> <head> <title>An Untestable Authentication Form</title> </head> <body> <form id="authentication_form"> <label for="username">Username:</label> <input type="text" id="username" name="username"></input> <label for="password">Password:</label> <input type="password" id="password" name="password"></input> <button>Submit</button> <p id="username_password_required" style="display: none;"> Both the username and password are required. </p> <p id="authentication_success" style="display: none;"> You have successfully authenticated! </p> <p id="authentication_failure" style="display: none;"> This username/password combination is not correct. </p> <p id="authentication_error" style="display: none;"> There was a problem authenticating the user, please try again later. </p> </form> <script src="jquery.min.js"></script> <!-- Inline Javascript is impossible to test from an external test harness --> <script> // Even if test harness was included in the HTML, Javascript is // inaccessible to tests $(function() { // Pyramid of doom - A mixture of disparate concerns and // very difficult to test individual parts $("#authentication_form").on("submit", function(event) { // Event handler logic is mixed with form handling logic event.preventDefault(); var username = $("#username").val(); var password = $("#password").val(); if (username && password) { // Without a mock, XHR requests require a functioning // back end, adding extra dependencies and delay $.ajax({ type: "POST", url: "/authenticate_user", data: { username: username, password: password }, success: function(data, status, jqXHR) { // Knowing when this completes requires some sort // of notification if (data.success) { $("#authentication_success").show(); } else { $("#authentication_failure").show(); } }, error: function(jqXHR, textStatus, errorThrown) { $("#authentication_error").show(); } }); } else { $("#username_password_required").show(); } }); }); </script> </body> </html> |
反模式使得应用的代码变的难以测试
1.内联Javascript – 嵌入在HTML文件中的Javascript代码是无法包含在外部单元测试工具中的。
2.无法复用的代码 – 即使Javascript代码单独放在外面,也没有提供公共的接口供其他人调用。
3.没有构造函数/原型对象 – 个人的单元测试就意味着独立的操作。测试一个单例是很困难的,因为一个测试的结果可能会影响到其他测试的结果。
4.金字塔厄运 – 深层的嵌套在Javascript开发中非常多见,但是他们是让人各种担忧的抓狂的东西。深层嵌套在内部的代码逻辑是很难进行单独测试的,并且随着时间的推移,会有变得像意大利面条式的难以维护的倾向。
5.拙劣的DOM事件处理程序 – 事件处理程序和表单提交逻辑混在一起,从而导致无法避免的担忧。
6.真正的XHR请求 – 真正的XHR请求需要一个可用的后端服务,前端和后端高速并行的开发是很困难的,因为XHR请求需要一个能工作的后端才能看到请求结果。
7.状态通知 – 更少的异步逻辑 – 没有某种形式的通知,是无法知道一个异步函数是什么时候执行完成的。
如何编写可测试的Javascript UI代码
上面列出的每一个问题都是可以解决的。稍微动下脑子思考一下,其实前端的代码是很容易测试的。
外链所有的Javascript代码
直接嵌入到一个HTML文件中的Javascript代码是无法被另一个HTML文件使用的。外链的Javascript代码是可复用的,并且可以被不止一个的HTML文件所引入。
提供一个公共接口
代码必须要提供公共接口才能被测试。在提供一个公共接口的时候,被用来封装逻辑的最经常使用的模式是使用模块。在Addy
Osmani的非常优秀的Javascript设计模式必知必会一书中,他指出:模块模式最初在传统软件行业中作为类的私有和公共接口的封装被提出的。
原来的样例应用程序没有公共接口,所有的代码都封装在一个自调用的私有函数中。唯一可以进行测试的地方就是表单提交事件功能部分。虽然确定是可以(进行测试)的,用仅有的混合事件处理程序来编写测试用例会有不必要的麻烦。
适当的封装模块可以用来限制功能访问,减少全局命名空间的污染,并且可以提供公共接口方便测试。
var PublicModule = (function() { "use strict"; // This is the public interface of the Module. var Module = { // publicFunction can be called externally publicFunction: function() { return "publicFunction can be invoked externally but " + privateFunction(); } }; // privateFunction is completely hidden from the outside. function privateFunction() { return "privateFunction cannot"; } return Module; }()); |
正如Addy指出的,模块模式的一个弊端在于“无法创建对私有成员的自动化单元测试”。一个函数如果不能被直接访问,那么它就不能被直接进行测试。模块设计的时候,在保持成员私有化和向公众公开成员之间存在一定的扩展性。
在Mozilla Persona代码库中,我们经常在测试公共接口的私有函数时暴露出困难,会很明显的把额外的函数作为测试API的一部分。虽然其他的开发者仍然可以调用这些私有函数,但作者的意图是很明显的。
... publicFunction: function() { return "publicFunction can be invoked externally but " + privateFunction(); } // BEGIN TESTING API , privateFunction: privateFunction // END TESTING API }; // privateFunction is now accessible via the TESTING API function privateFunction() { ... |
在注释标记// BEGIN TESTING API和//END TESTING API 之间的代码可以在项目构建的时候删除掉。
使用可实例化的对象
最初的应用程序并没有使用可实例化的对象,它的代码被设计的只执行一次。这一约束使得重置应用状态以及独立的执行单元测试变得很困难。
测试可以被多次初始化的模块相对来说是更容易的。在Javascript中,存在两个相似的方法:构造函数和Object.create.
前面两个例子中的PublicModule变量是对象而不是函数,而Object.create可以被用来创建对象的副本。可以在原型中增加一个可选的初始化函数以便在构造函数中执行初始化。
... // the init function takes care of initialization traditionally done // in a constructor init: function(options) { this.valueSetOnInit = options.valueSetOnInit; }, publicFunction: function() { ... 1 2 3 4 5 // create an instance of the PublicModule. var objInstance = Object.create(PublicModule); objInstance.init({ valueSetOnInit: "value set during initialization" }); |
减少嵌套回调
非常不幸的是,嵌套回调是前端Javascript编程很重要的一部分。上面不可测试验证的Form表单的例子绝没有额外的包含三层嵌套的回调。深层的嵌套回调代码是这样的–他们将功能糟糕的混杂在一起并且让人担忧重重。
将金字塔式的代码拆分为各功能组件我们可以得到较“平坦”的代码,这些代码会由小的,有粘着力的以及功能易测试的代码组成。
将DOM事件处理程序和它的行为分离
不可测试验证的Form例子用了一个单独的提交处理程序来同时关注事件处理和表单提交。不仅仅是同时关注了这两件事,而且这个混合结果导致如果不使用混合的事件程序将无法提交表单。
... $("form").on("submit", function(event) { event.preventDefault(); // this code is impossible to invoke programmatically // without using a synthetic DOM event. var name = $("#name").val(); doSomethingWithName(name); }); ... |
将表单处理逻辑从事件处理程序中分离出来让我们可以编程提交表单而不用求助于混合的事件处理程序。
... $("form").on("submit", submitHandler); function submitHandler(event) { event.preventDefault(); submitForm(); }); // form submission can now be done programmatically // by calling submitForm directly. function submitForm() { var name = $("#name").val(); doSomethingWithName(name); } ... |
单元测试可以使用submitForm而不必使用混合的表单提交事件处理程序。
模拟XHR请求
几乎所有的现代网站都是用XHR(AJAX)请求。XHR请求依赖于服务端;从前端的请求必须被服务端响应,否则应用什么都做不了。只有服务端也准备好了才能测试真正的XHR请求,否则会严重影响并行开发。
... // This is an explicit dependency on the jQuery ajax functionality as well // as a working back end. $.ajax({ type: "POST", url: "/authenticate_user", data: { username: username, password: password }, success: function(data, status, jqXHR) { ... |
与其执行真正的XHR请求,不如用一种格式定义良好的XHR响应来模拟。Mock对象是一种以可控的方式来模拟真正对象的行为的模拟对象。模拟对象经常被用于那些需要依赖不可获得的、较慢的、不可控的或者缺陷太多而无法信任的功能上面。XHR请求恰好是一个很好的例子。
同时测试前端和后端是很重要的,但是这最好留给功能测试。单元测试意味着测试单独的条目。
一个发起XHR请求的模块应该接受一个包含在其构造函数或者初始化函数中的XHR模拟对象。然后这个模块使用这个被包含的模拟对象而不是去直接调用$.ajax。模块在执行单元测试的时候使用模拟对象,但是在生产中使用$.ajax。
合理的默认值可以减少生产体系中初始化代码的数量。
... init: function(options) { // Use the injected ajax function if available, otherwise // use $.ajax by default. this.ajax = options.ajax || $.ajax; }, submitForm: function() { ... // This can call either an XHR mock or a production XHR resource // depending on how the object is initialized. this.ajax({ type: "POST", url: "/authenticate_user", data: { username: username, password: password }, ... }); } ... |
异步编程需要通知机制
上面不可测试验证的Form表单的例子缺少通知的机制来表明什么时候所有的进程已结束。这在异步函数运行结束后的需要执行的单元测试中会是一个问题。
Javascript中存在很多的通知机制,回调,观察者模式以及事件是几个。简单的回调函数是目前最常用的。
... submitForm: function(done) { ... this.ajax({ ... // an ajax call is asynchronous. When it successfully completes, // it calls the done function. success: done }); } ... |
单元测试后清除不必要的代码
单元测试应该单独的进行;一旦一个单元测试结束,所有的测试状态应该被清除,包括DOM事件处理。导致对象将DOM事件处理程序绑定到相同的DOM元素的两个测试用例容易相互影响,而这容易被开发者疏忽。为了排除这种影响,一个没用的对象应该从它的DOM事件处理程序中移除。额外的工作会提供一些额外的好处;在应用中创建和销毁对象可以大大的减少内存溢出。
... teardown: teardown() { $("form").off("submit", submitHandler); } ... |
总结
就是这样。实际上没有编写多少前端Javascript代码,这样就可以进行单元测试。公共接口,初始化对象,少嵌套的代码结构,组织良好的事件处理程序以及测试之后不必要代码的清除。
编写可测试的Javascript代码(2):从反模式进行重构
这是介绍“如何编写可测试的Javascript UI代码”两篇文章中的第二篇。
在第一篇文章反模式及其它们的解决方案中用一个示例应用程序引入了几个常见的,可避免的,约束了可测试性的反模式,并且解释了为什么这些常见的做法是反模式的以及如何修复他们。
这篇文章来继续重构原来的应用,以使得它的代码更容易阅读,更容易被复用以及更容易进行测试。一旦重构完成,测试就要开始:创建测试工具,开发XHR模拟,最后,添加一个完整的单元测试用例。
使用最佳实践来编写可测试的UI代码
在第一篇文章“反模式及其解决方案”中,列出了几个使得UI代码可测试的最佳实践:
1.外链所有的Javascript;
2.提供公共接口;
3.使用可实例化的对象;
4.减少嵌套回调;
5.将DOM事件处理程序与事件处理函数相分离;
6.当异步函数完成的时候通知订阅者;
7.测试完成后清除没用的对象;
8.在XHR请求中添加模拟对象;
9.把应用初始化分离成自己的模块。
用这个清单作为指引,原来的应用程序被会完全重构,并能达到我们对代码进行单元测试的目的。
从HTML开始–外链所有的脚本
原来内联在HTML文件中的Javascript代码已经被放在了外部并且被放置于两个文件中:authentication-form.js
和start.js。原来的大部分的逻辑放于authentication-form.js模块中,应用的初始化则在start.js中进行。
引用自index.html
<!DOCTYPE html> <html> <head> <title>A Testable Authentication Form</title> </head> <body> ... <script src="jquery.min.js"></script> <!-- Both the authentication form and the initialization code are split into their own files. Javascript resources can be combined for production use. --> <script src="authentication-form.js"></script> <script src="start.js"></script> </body> </html> |
拥有可选的公共接口的逻辑封装模块
AuthenticationForm是一个公共的可获取的模块,该模块比较简洁地封装了大部分的原始逻辑。AuthenticationForm提供了一个公共的接口,通过该接口其功能可以被测试。
一个公共接口–引用自authentication-form.js
// The Module pattern is used to encapsulate logic. AuthenticationForm is the // public interface. var AuthenticationForm = (function() { "use strict"; ... |
使用可初始化的对象
原来的form表单例子没有可实例化的部分,这也就意味着它的代码只能运行一次。这样的话有效的单元测试几乎是不可能的。重构了的AuthenticationForm是一个原型对象,使用Object.create来创建新的实例。
可实例化的对象-引用自authentication-form.js
var AuthenticationForm = (function() { "use strict"; // Module is the prototype object that is assigned to // AuthenticationForm. New instances of AuthenticationForm // are created using: // // var authForm = Object.create(AuthenticationForm) // var Module = { init: function(options) { ... }; return Module; ... }()); |
减少嵌套回调的使用
重构的AuthenticationForm从原来的的深层嵌套回调(导致代码成金字塔状)抽取逻辑形成四个公共的可以获取得到的函数。这些函数中的两个被用来提供给对象初始化和销毁,其余的两个用于测试接口。
去除金字塔–引用自authentication-form.js
... var Module = { init: ... teardown: ... // BEGIN TESTING API submitForm: submitForm, checkAuthentication: checkAuthentication // END TESTING API }; ... |
将DOM事件处理程序从事件行为中分离出来
将DOM事件处理程序从事件行为中分离出来有助于代码的重用和可测试。
将DOM事件处理程序从事件行为中分离出来–引用自authentication-form.js
... init: function(options) { ... // A little bit of setup is needed for teardown. This will be // explained shortly. this.submitHandler = onFormSubmit.bind(this); $("#authentication_form").on("submit", this.submitHandler); }, ... }; ... // Separate the submit handler from the actual action. This allows // onFormSubmit takes care of the event then calls submitForm like any // other function would. function onFormSubmit(event) { event.preventDefault(); submitForm.call(this); } // submitForm to be called programatically without worrying about // handling the event. function submitForm(done) { ... } |
在异步函数中使用回调(或者其他的通知机制)
AuthenticationForm的测试接口中的两个函数,submitForm和checkAuthentication是异步的。当所有处理程序都完成的时候他们都接受一个函数进行回调。
有回调函数的异步回调–引用自authentication-form.js
}; ... // checkAuthentication is asynchronous but the unit tests need to // perform their checks after all actions are complete. "done" is an optional // callback that is called once all other actions complete. function submitForm(done) { ... } // checkAuthentication makes use of the ajax mock for unit testing. function checkAuthentication(username, password, done) { ... } ... |
将没用的对象处理掉
单元测试应该独立的进行。任何的状态,包括附属的DOM事件处理器,在测试的时候必须被重置。
移除附加的DOM事件处理程序–引用自authentication-form.js
... init: function(options) { ... // If unit tests are run multiple times, it is important to be able to // detach events so that one test run does not interfere with another. this.submitHandler = onFormSubmit.bind(this); $("#authentication_form").on("submit", this.submitHandler); }, teardown: function() { // detach event handlers so that subsequent test runs do not interfere // with each other. $("#authentication_form").off("submit", this.submitHandler); }, ... |
将应用的初始化逻辑分离出一个单独的(初始化)模块
start.js是一个自调用的函数,它在多有的js文件都下载完成后执行。因为我们的应用很简单,只需要很少的初始化代码–一个AuthenticationForm实例被创建并初始化。
start.js
(function() { "use strict"; var authenticationForm = Object.create(AuthenticationForm); authenticationForm.init(); }()); |
在这一点上,原来的整个应用程序被重构并且重新实现了。用户应该能看到在功能上并没有做改变,纯粹是代码结构的修改。
如何进行单元测试?
尽管当前我们的代码是可测试的,一篇关于单元测试的文章却没有写任何相关的测试代码!有几个高质量的测试框架,在这个例子中我们使用QUnit。
首先,我们需要一个测试工具。一个测试工具由一个模拟DOM和Javascript代码组成。模拟DOM由测试中要使用的元素组成,通常是类似于form或者你要检测可见性的元素这些东西。为了避免测试交叉污染,在每一个单元测试之后都将DOM元素进行重置。QUnit期望模拟元素包含在id为#qunit-fixture的元素中。
Javascript代码包含一个单元测试运行器,要被测试的代码,独立的模拟以及对他们自己的一些测试。
测试工具–引用自tests/index.html
... <h1 id="qunit-header">Authentication Form Test Suite</h1> ... <!-- A slimmed down form mock is used so there are form elements to attach event handlers to --> <div id="qunit-fixture"> <form> <input type="text" id="username" name="username"></input> <input type="password" id="password" name="password"></input> </form> <p id="username_password_required" style="display: none;"> Both the username and password are required. </p> ... </div> <!-- Javascript used for testing --> <script src="qunit.js"></script> <!-- Include the ajax mock so no XHR requests are actually made --> <script src="ajax-mock.js"></script> <!-- Include the module to test --> <script src="../authentication-form.js"></script> <!-- The tests --> <script src="authentication-form.js"></script> ... |
书写XHR模拟
XHR请求需要依赖于服务端,从前端发起的请求必须被服务端响应否则应用什么都干不了。用真正的XHR请求进行测试意味着服务端必须做好准备,这会严重阻碍前后端并行开发。
与其发起真正的XHR请求,不如使用一个模拟的请求来做。模拟对象是一些替代对象–可以在测试中进行精确地控制。一个模拟对象必须实现用户要使用的所有功能。幸运的是,XHR模拟(也叫AjaxMock)只需要实现所有jQuery.ajax功能的很小的一部分即可。这个单独的模拟功能提供了整合所有服务端响应的能力。几个额外的函数被加进来辅助单元测试。
AjaxMock接口
AjaxMock = (function() { ... /* * AjaxMock mimicks portions of the $.ajax functionality. * See http://api.jquery.com/jQuery.ajax/ */ var AjaxMock = { // The only jQuery function that is needed by the consumer ajax: function(options) { ... }, // What follows are non standard functions used for testing. setSuccess: ... setError: ... getLastType: ... getLastURL: ... getLastData: ... }; return AjaxMock; }()); |
完成一些测试!
现在,测试工具和XHR模拟都已经准备好了,我们可以写一些单元测试了!测试包含6个独立的测试。每个测试都会实例化一个新的AuthenticationForm对象和XHR模拟。XHR模拟可以为每一个可能的后端响应编写测试。
(function() { "use strict"; var ajaxMock, authenticationForm; module("testable-authentication-form", { setup: function() { // create a mock XHR object to inject into the authenticationForm for // testing. ajaxMock = Object.create(AjaxMock); authenticationForm = Object.create(AuthenticationForm); authenticationForm.init({ // Inject the ajax mock for unit testing. ajax: ajaxMock.ajax.bind(ajaxMock) }); }, teardown: function() { // tear down the authenticationForm so that subsequent test runs do not // interfere with each other. authenticationForm.teardown(); authenticationForm = null; } }); asyncTest("submitForm with valid username and password", function() { $("#username").val("testuser"); $("#password").val("password"); ajaxMock.setSuccess({ success: true, username: "testuser", userid: "userid" }); authenticationForm.submitForm(function(error) { equal(error, null); ok($("#authentication_success").is(":visible")); start(); }); }); ... }()); |
总结
花了一些时间,但是我们达到了我们的目的。我们的代码易于阅读,易于重用,并且有一个完整的测试套件。
编写可测试的代码通常是一个挑战,但是你一旦适应,基础的部分还是很容易的。在开始一行代码之前,你要问你自己“我要如何来对代码进行测试?”。这个简单的问题最终将节省大量时间和并在你重构或添加新功能的时候给你信心。
最终产品
index.html
<!DOCTYPE html> <!-- /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ --> <html> <head> <title>A Testable Authentication Form</title> </head> <body> <form id="authentication_form"> <label for="username">Username:</label> <input type="text" id="username" name="username"></input> <label for="password">Password:</label> <input type="password" id="password" name="password"></input> <button>Submit</button> <p id="username_password_required" style="display: none;"> Both the username and password are required. </p> <p id="authentication_success" style="display: none;"> You have successfully authenticated! </p> <p id="authentication_failure" style="display: none;"> This username/password combination is not correct. </p> <p id="authentication_error" style="display: none;"> There was a problem authenticating the user, please try again later. </p> </form> <script src="jquery.min.js"></script> <!-- Both the authentication form and the initialization code are split into their own files. They can be combined for production use. --> <script src="authentication-form.js"></script> <script src="start.js"></script> </body> </html> |
authentication-form.js
// The Module pattern is used to encapsulate logic. AuthenticationForm is the // public interface. var AuthenticationForm = (function() { "use strict"; var Module = { init: function(options) { options = options || {}; // Use an injected request function for testing, use jQuery's xhr // function as a default. this.ajax = options.ajax || $.ajax; // If unit tests are run multiple times, it is important to be able to // detach events so that one test run does not interfere with another. this.submitHandler = onFormSubmit.bind(this); $("#authentication_form").on("submit", this.submitHandler); }, teardown: function() { // detach event handlers so that subsequent test runs do not interfere // with each other. $("#authentication_form").off("submit", this.submitHandler); }, // BEGIN TESTING API // A build script could strip this out to save bytes. submitForm: submitForm, checkAuthentication: checkAuthentication // END TESTING API }; return Module; // Separate the submit handler from the actual action. This allows // submitForm to be called programatically without worrying about // handling the event. function onFormSubmit(event) { event.preventDefault(); submitForm.call(this); } // checkAuthentication is asynchronous but the unit tests need to // perform their checks after all actions are complete. "done" is an // optional callback that is called once all other actions complete. function submitForm(done) { var username = $("#username").val(); var password = $("#password").val(); if (username && password) { checkAuthentication.call(this, username, password, function(error, user) { if (error) { $("#authentication_error").show(); } else { updateAuthenticationStatus(user); } // surface any errors so tests can be done. done && done(error); }); } else { $("#username_password_required").show(); // pass back an error message that can be used for testing. done && done("username_password_required"); } } // checkAuthentication makes use of the ajax mock for unit testing. function checkAuthentication(username, password, done) { this.ajax({ type: "POST", url: "/authenticate_user", data: { username: username, password: password }, success: function(resp) { var user = null; if (resp.success) { user = { username: resp.username, userid: resp.userid }; } done && done(null, user); }, error: function(jqXHR, textStatus, errorThrown) { done && done(errorThrown); } }); } function updateAuthenticationStatus(user) { if (user) { $("#authentication_success").show(); } else { $("#authentication_failure").show(); } } }()); |
start.js
(function() { "use strict"; var authenticationForm = Object.create(AuthenticationForm); authenticationForm.init(); }()); |
tests/index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link type="text/css" rel="stylesheet" href="qunit.css" /> </head> <body> <h1 id="qunit-header">Authentication Form Test Suite</h1> <h2 id="qunit-banner"></h2> <div id="qunit-testrunner-toolbar"></div> <h2 id="qunit-userAgent"></h2> <ol id="qunit-tests"></ol> <div id="qunit-test-area"></div> <div id="qunit-fixture"> <!-- A slimmed down form mock is used so there are form elements to attach event handlers to --> <form> <input type="text" id="username" name="username"></input> <input type="password" id="password" name="password"></input> </form> <p id="username_password_required" style="display: none;"> Both the username and password are required. </p> <p id="authentication_success" style="display: none;"> You have successfully authenticated! </p> <p id="authentication_failure" style="display: none;"> This username/password combination is not correct. </p> <p id="authentication_error" style="display: none;"> There was a problem authenticating the user, please try again later. </p> </div> <script src="../jquery.min.js"></script> <!-- QUnit is used for this example. Jasmine and Mocha are two other popular test suites --> <script src="qunit.js"></script> <!-- Include the ajax mock so no XHR requests are actually made --> <script src="ajax-mock.js"></script> <!-- Include the module to test --> <script src="../authentication-form.js"></script> <!-- The tests --> <script src="authentication-form.js"></script> </body> </html> |
tests/ajax-mock.js
AjaxMock = (function() { "use strict"; /* * The AjaxMock object type is a controllable XHR module used for unit * testing. It is injected into the AuthenticationForm so that real XHR * requests are not made. Instead, the mock can be controlled to return * expected values. * * AjaxMock mimicks the portions of the $.ajax functionality. * See http://api.jquery.com/jQuery.ajax/ */ var AjaxMock = { // The only jQuery function used for ajax requests ajax: function(options) { this.type = options.type; this.url = options.url; this.data = options.data; if ("successValue" in this) { // Neither our code nor our tests make use of jqXHR or textStatus if (options.success) options.success(this.successValue); } else if ("errorValue" in this) { // Neither our code nor our tests make use of jqXHR or textStatus if (options.error) options.error(null, 500, this.errorValue); } else { throw new Error("setSuccess or setError must be called before ajax"); } }, // What follows are non standard functions used for testing. setSuccess: function(successValue) { this.successValue = successValue; }, setError: function(errorValue) { this.errorValue = errorValue; }, getLastType: function() { return this.type; }, getLastURL: function() { return this.url; }, getLastData: function() { return this.data; } }; return AjaxMock; }()); |
tests/authentication-form.js
(function() { "use strict"; var ajaxMock, authenticationForm; module("testable-authentication-form", { setup: function() { // create a mock XHR object to inject into the authenticationForm for // testing. ajaxMock = Object.create(AjaxMock); authenticationForm = Object.create(AuthenticationForm); authenticationForm.init({ // Inject the ajax mock for unit testing. ajax: ajaxMock.ajax.bind(ajaxMock) }); }, teardown: function() { // tear down the authenticationForm so that subsequent test runs do not // interfere with each other. authenticationForm.teardown(); authenticationForm = null; } }); asyncTest("submitForm with valid username and password", function() { $("#username").val("testuser"); $("#password").val("password"); ajaxMock.setSuccess({ success: true, username: "testuser", userid: "userid" }); authenticationForm.submitForm(function(error) { equal(error, null); ok($("#authentication_success").is(":visible")); start(); }); }); asyncTest("submitForm with invalid username and password", function() { $("#username").val("testuser"); $("#password").val("invalidpassword"); ajaxMock.setSuccess({ success: false }); authenticationForm.submitForm(function(error) { equal(error, null); ok($("#authentication_failure").is(":visible")); start(); }); }); asyncTest("submitForm with missing username and password", function() { $("#username").val(""); $("#password").val(""); authenticationForm.submitForm(function(error) { equal(error, "username_password_required"); ok($("#username_password_required").is(":visible")); start(); }); }); asyncTest("submitForm with XHR error", function() { $("#username").val("testuser"); $("#password").val("password"); ajaxMock.setError("could not complete"); authenticationForm.submitForm(function(error) { equal(error, "could not complete"); ok($("#authentication_error").is(":visible")); start(); }); }); asyncTest("checkAuthentication with valid user", function() { ajaxMock.setSuccess({ success: true, username: "testuser", userid: "userid" }); authenticationForm.checkAuthentication("testuser", "password", function(error, user) { equal(error, null); equal(ajaxMock.getLastType(), "POST"); equal(ajaxMock.getLastURL(), "/authenticate_user"); var data = ajaxMock.getLastData(); equal(data.username, "testuser"); equal(data.password, "password"); equal(user.username, "testuser"); equal(user.userid, "userid"); start(); }); }); asyncTest("checkAuthentication with missing XHR error", function() { ajaxMock.setError("could not complete"); authenticationForm.checkAuthentication("testuser", "password", function(error) { equal(error, "could not complete"); start(); }); }); }()); |
|