±àд¿É²âÊÔµÄ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 ¨C ǶÈëÔÚHTMLÎļþÖеÄJavascript´úÂëÊÇÎÞ·¨°üº¬ÔÚÍⲿµ¥Ôª²âÊÔ¹¤¾ßÖеġ£
2.ÎÞ·¨¸´ÓõĴúÂë ¨C ¼´Ê¹Javascript´úÂëµ¥¶À·ÅÔÚÍâÃæ£¬Ò²Ã»ÓÐÌṩ¹«¹²µÄ½Ó¿Ú¹©ÆäËûÈ˵÷Óá£
3.ûÓй¹Ô캯Êý/ÔÐͶÔÏó ¨C ¸öÈ˵ĵ¥Ôª²âÊÔ¾ÍÒâζ×ŶÀÁ¢µÄ²Ù×÷¡£²âÊÔÒ»¸öµ¥ÀýÊǺÜÀ§Äѵģ¬ÒòΪһ¸ö²âÊԵĽá¹û¿ÉÄÜ»áÓ°Ïìµ½ÆäËû²âÊԵĽá¹û¡£
4.½ð×ÖËþ¶òÔË ¨C Éî²ãµÄǶÌ×ÔÚJavascript¿ª·¢Öзdz£¶à¼û£¬µ«ÊÇËûÃÇÊÇÈÃÈ˸÷ÖÖµ£ÓǵÄ×¥¿ñµÄ¶«Î÷¡£Éî²ãǶÌ×ÔÚÄÚ²¿µÄ´úÂëÂß¼ÊǺÜÄѽøÐе¥¶À²âÊԵ쬲¢ÇÒËæ×Åʱ¼äµÄÍÆÒÆ£¬»áÓбäµÃÏñÒâ´óÀûÃæÌõʽµÄÄÑÒÔά»¤µÄÇãÏò¡£
5.×¾ÁÓµÄDOMʼþ´¦Àí³ÌÐò ¨C ʼþ´¦Àí³ÌÐòºÍ±íµ¥Ìá½»Âß¼»ìÔÚÒ»Æð£¬´Ó¶øµ¼ÖÂÎÞ·¨±ÜÃâµÄµ£ÓÇ¡£
6.ÕæÕýµÄXHRÇëÇó ¨C ÕæÕýµÄXHRÇëÇóÐèÒªÒ»¸ö¿ÉÓõĺó¶Ë·þÎñ£¬Ç°¶ËºÍºó¶Ë¸ßËÙ²¢ÐеĿª·¢ÊǺÜÀ§Äѵģ¬ÒòΪXHRÇëÇóÐèÒªÒ»¸öÄܹ¤×÷µÄºó¶Ë²ÅÄÜ¿´µ½ÇëÇó½á¹û¡£
7.״̬֪ͨ ¨C ¸üÉÙµÄÒì²½Âß¼ ¨C ûÓÐijÖÖÐÎʽµÄ֪ͨ£¬ÊÇÎÞ·¨ÖªµÀÒ»¸öÒì²½º¯ÊýÊÇʲôʱºòÖ´ÐÐÍê³ÉµÄ¡£
ÈçºÎ±àд¿É²âÊÔµÄ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±íµ¥µÄÀý×Ó¾øÃ»ÓжîÍâµÄ°üº¬Èý²ãǶÌ׵Ļص÷¡£Éî²ãµÄǶÌ׻ص÷´úÂëÊÇÕâÑùµÄ¨CËûÃǽ«¹¦ÄÜÔã¸âµÄ»ìÔÓÔÚÒ»Æð²¢ÇÒÈÃÈ˵£ÓÇÖØÖØ¡£
½«½ð×ÖËþʽµÄ´úÂë²ð·ÖΪ¸÷¹¦ÄÜ×é¼þÎÒÃÇ¿ÉÒԵõ½½Ï¡°Æ½Ì¹¡±µÄ´úÂ룬ÕâЩ´úÂë»áÓÉСµÄ£¬ÓÐÕ³×ÅÁ¦µÄÒÔ¼°¹¦ÄÜÒײâÊԵĴúÂë×é³É¡£
½«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 }); } ... |
µ¥Ôª²âÊÔºóÇå³ý²»±ØÒªµÄ´úÂë
µ¥Ôª²âÊÔÓ¦¸Ãµ¥¶ÀµÄ½øÐУ»Ò»µ©Ò»¸öµ¥Ôª²âÊÔ½áÊø£¬ËùÓеIJâÊÔ״̬Ӧ¸Ã±»Çå³ý£¬°üÀ¨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¿ªÊ¼¨CÍâÁ´ËùÓеĽű¾
ÔÀ´ÄÚÁªÔÚHTMLÎļþÖеÄJavascript´úÂëÒѾ±»·ÅÔÚÁËÍⲿ²¢ÇÒ±»·ÅÖÃÓÚÁ½¸öÎļþÖУºauthentication-form.js
ºÍstart.js¡£ÔÀ´µÄ´ó²¿·ÖµÄÂß¼·ÅÓÚauthentication-form.jsÄ£¿éÖУ¬Ó¦Óõijõʼ»¯ÔòÔÚ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ÌṩÁËÒ»¸ö¹«¹²µÄ½Ó¿Ú£¬Í¨¹ý¸Ã½Ó¿ÚÆä¹¦ÄÜ¿ÉÒÔ±»²âÊÔ¡£
Ò»¸ö¹«¹²½Ó¿Ú¨CÒýÓÃ×Ôauthentication-form.js
// The Module pattern is used to encapsulate logic. AuthenticationForm is the // public interface. var AuthenticationForm = (function() { "use strict"; ... |
ʹÓÿɳõʼ»¯µÄ¶ÔÏó
ÔÀ´µÄform±íµ¥Àý×ÓûÓпÉʵÀý»¯µÄ²¿·Ö£¬ÕâÒ²¾ÍÒâζ×ÅËüµÄ´úÂëÖ»ÄÜÔËÐÐÒ»´Î¡£ÕâÑùµÄ»°ÓÐЧµÄµ¥Ôª²âÊÔ¼¸ºõÊDz»¿ÉÄܵġ£Öع¹Á˵Ä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´ÓÔÀ´µÄµÄÉî²ãǶÌ׻ص÷£¨µ¼Ö´úÂë³É½ð×ÖËþ×´£©³éÈ¡Âß¼ÐγÉËĸö¹«¹²µÄ¿ÉÒÔ»ñÈ¡µÃµ½µÄº¯Êý¡£ÕâЩº¯ÊýÖеÄÁ½¸ö±»ÓÃÀ´Ìṩ¸ø¶ÔÏó³õʼ»¯ºÍÏú»Ù£¬ÆäÓàµÄÁ½¸öÓÃÓÚ²âÊÔ½Ó¿Ú¡£
È¥³ý½ð×ÖËþ¨CÒýÓÃ×Ôauthentication-form.js
... var Module = { init: ... teardown: ... // BEGIN TESTING API submitForm: submitForm, checkAuthentication: checkAuthentication // END TESTING API }; ... |
½«DOMʼþ´¦Àí³ÌÐò´ÓʼþÐÐΪÖзÖÀë³öÀ´
½«DOMʼþ´¦Àí³ÌÐò´ÓʼþÐÐΪÖзÖÀë³öÀ´ÓÐÖúÓÚ´úÂëµÄÖØÓúͿɲâÊÔ¡£
½«DOMʼþ´¦Àí³ÌÐò´ÓʼþÐÐΪÖзÖÀë³öÀ´¨CÒýÓÃ×Ô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ÊÇÒì²½µÄ¡£µ±ËùÓд¦Àí³ÌÐò¶¼Íê³ÉµÄʱºòËûÃǶ¼½ÓÊÜÒ»¸öº¯Êý½øÐлص÷¡£
Óлص÷º¯ÊýµÄÒì²½»Øµ÷¨CÒýÓÃ×Ô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ʼþ´¦Àí³ÌÐò¨CÒýÓÃ×Ô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); }, ... |
½«Ó¦Óõijõʼ»¯Âß¼·ÖÀë³öÒ»¸öµ¥¶ÀµÄ£¨³õʼ»¯£©Ä£¿é
start.jsÊÇÒ»¸ö×Ôµ÷Óõĺ¯Êý£¬ËüÔÚ¶àÓеÄjsÎļþ¶¼ÏÂÔØÍê³ÉºóÖ´ÐС£ÒòΪÎÒÃǵÄÓ¦Óúܼòµ¥£¬Ö»ÐèÒªºÜÉٵijõʼ»¯´úÂë¨CÒ»¸öAuthenticationFormʵÀý±»´´½¨²¢³õʼ»¯¡£
start.js
(function() { "use strict"; var authenticationForm = Object.create(AuthenticationForm); authenticationForm.init(); }()); |
ÔÚÕâÒ»µãÉÏ£¬ÔÀ´µÄÕû¸öÓ¦ÓóÌÐò±»Öع¹²¢ÇÒÖØÐÂʵÏÖÁË¡£Óû§Ó¦¸ÃÄÜ¿´µ½ÔÚ¹¦ÄÜÉϲ¢Ã»ÓÐ×ö¸Ä±ä£¬´¿´âÊÇ´úÂë½á¹¹µÄÐ޸ġ£
ÈçºÎ½øÐе¥Ôª²âÊÔ£¿
¾¡¹Üµ±Ç°ÎÒÃǵĴúÂëÊǿɲâÊԵģ¬Ò»Æª¹ØÓÚµ¥Ôª²âÊÔµÄÎÄÕÂȴûÓÐдÈκÎÏà¹ØµÄ²âÊÔ´úÂ룡Óм¸¸ö¸ßÖÊÁ¿µÄ²âÊÔ¿ò¼Ü£¬ÔÚÕâ¸öÀý×ÓÖÐÎÒÃÇʹÓÃQUnit¡£
Ê×ÏÈ£¬ÎÒÃÇÐèÒªÒ»¸ö²âÊÔ¹¤¾ß¡£Ò»¸ö²âÊÔ¹¤¾ßÓÉÒ»¸öÄ£ÄâDOMºÍJavascript´úÂë×é³É¡£Ä£ÄâDOMÓɲâÊÔÖÐҪʹÓõÄÔªËØ×é³É£¬Í¨³£ÊÇÀàËÆÓÚform»òÕßÄãÒª¼ì²â¿É¼ûÐÔµÄÔªËØÕâЩ¶«Î÷¡£ÎªÁ˱ÜÃâ²âÊÔ½»²æÎÛȾ£¬ÔÚÿһ¸öµ¥Ôª²âÊÔÖ®ºó¶¼½«DOMÔªËØ½øÐÐÖØÖá£QUnitÆÚÍûÄ£ÄâÔªËØ°üº¬ÔÚidΪ#qunit-fixtureµÄÔªËØÖС£
Javascript´úÂë°üº¬Ò»¸öµ¥Ôª²âÊÔÔËÐÐÆ÷£¬Òª±»²âÊԵĴúÂ룬¶ÀÁ¢µÄÄ£ÄâÒÔ¼°¶ÔËûÃÇ×Ô¼ºµÄһЩ²âÊÔ¡£
²âÊÔ¹¤¾ß¨CÒýÓÃ×Ô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ÇëÇ󣬲»ÈçʹÓÃÒ»¸öÄ£ÄâµÄÇëÇóÀ´×ö¡£Ä£Äâ¶ÔÏóÊÇÒ»Ð©Ìæ´ú¶ÔÏó¨C¿ÉÒÔÔÚ²âÊÔÖнøÐо«È·µØ¿ØÖÆ¡£Ò»¸öÄ£Äâ¶ÔÏó±ØÐëʵÏÖÓû§ÒªÊ¹ÓõÄËùÓй¦ÄÜ¡£ÐÒÔ˵ÄÊÇ£¬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(); }); }); }()); |
|