ptg 1 Automated Testing A s web developers it is easy to find ourselves in situations where we spend un- healthy amounts of time with the refresh button in our browsers. You know the drill: type some code in your text editor, Alt+Tab to the browser, hit F5. Lather, rinse, repeat. This sort of manual testing is time-consuming, error-prone, and irrepro- ducible. Given that our web applications are expected to run on a vast combination of browsers and platforms, testing them all manually will inevitably become an impossible task. So we focus on a few combinations and perform the occasional check-up on the broader selection. The end result is an unsatisfactory development process and possibly brittle solutions. Over the yearslots of tools have emerged to improve our lives as web developers. We now have developer tools for all the major browsers, there are several JavaScript debuggers to choose from, and even IDEs to spot typos and other mistakes. Spend- ing some time in Firefox’s Firebug plugin interacting with an application sure beats those pesky alerts, but we’re still stuck with a manual, error-prone, and time- consuming debugging process. Humans are lazy, programmers even more so. When manual processes slow us down, we seek to automate the manual behavior, allowing us to spend our time doing something meaningful. In fact, as web developers, our job is more often than not to automate some tedious task in order to improve business value. Online banking is a great example—instead of going to the bank, standing in line and interacting 3 From the Library of WoweBook.Com Download from www.eBookTM.com ptg 4 Automated Testing with another human to move some cash from account A to account B, we simply log in from the comfort of our couch and get the job done in a couple of minutes. Saves us time and saves the bank time. Automated testing provides a solution to the manual testing process. Instead of filling out that form one more time and hitting submit to see if the client-side validations trigger as expected, we can instruct software to perform this test for us. The advantages are obvious: given a convenient way to run the automated test we can test in numerous browsers with a single effort, we can rerun the test at any later stage, and the test may even run on some schedule that requires no manual interaction whatsoever. Automated software testing has been around for quite a while, even for JavaScript. JsUnit dates back to 2001, Selenium came along in 2004, and since then an incredible amount of tools have emerged. Still, automated testing seems to have less momentum in the JavaScript/web development community than most other programming communities. In this chapter we’ll investigate one means to automate software testing, the unit test, and how it applies to the world of JavaScript. 1.1 The Unit Test A unit test is a piece of code that tests a piece of production code. It does so by setting up one or a few more objects in a known state, exercising them (e.g., calling a method), and then inspecting the result, comparing it to the expected outcome. Unit tests are stored on disk and should be easy and fast to run; if tests are hard or slow to run, developers are less likely to run them. Unit tests should test software components in isolation. They should also run isolated—no test should ever depend on another test, tests should be able to run simultaneously and in any order. In order to test components in isolation, it is sometimes necessary to mock or stub their dependencies. We will discuss mocking and stubbing in context in Part III, Real-World Test-Driven Development in JavaScript and in more detail in Chapter 16, Mocking and Stubbing. Having unit tests stored on disk, and usually stored in version control along with the production code, means we can run tests at any time: • When the implementation is complete, to verify its correct behavior • When the implementation changes, to verify its behavior is intact • When new units are added to the system, to verify it still fulfills its intended purpose From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.1 The Unit Test 5 1.1.1 Unit Testing Frameworks Unit tests are usually written using a unit testing framework, although that is not strictly necessary. In this chapter we’ll focus on the concept of unit tests, working through the different aspects of writing and running them. We’ll defer the discussion of actual testing frameworks for JavaScript to Chapter 3, Tools of the Trade. It’s likely that you’ve already written more than a few unit tests, even if you have never done any structured unit testing. Whenever you pop up a console in a browser (e.g., Firebug, Safari’s Inspector or others) to debug or play live with your code, you probably issue some statements and inspect the resulting state of the involved objects. In many cases this is unit testing, only it isn’t automated and it’s not reproducible. We’ll work through an example of this kind of testing and gradually formalize it as an xUnit test case. xUnit is a common way to refer to test frameworks that are either a direct port of JUnit, or more loosely based on the ideas and concepts in it—or, more correctly, the ideas and concepts in SUnit, the Smalltalk testing framework. Kent Beck, the father of extreme programming, played an integral part in the creation of both these frameworks, and even though SUnit was the first implementation, JUnit has done the most in terms of popularizing the pattern. 1.1.2 strftime for JavaScript Dates Many programming languages provide a strftime function or similar. It operates on a date or timestamp, accepts a format string, and produces a formatted string that represents the date. For example, in Ruby, strftime is available as a method on time and date objects, and Listing 1.1 shows an example of its use. Listing 1.1 Time#strftime in Ruby Time.now.strftime("Printed on %m/%d/%Y") #=> "Printed on 09/09/2010" Listing 1.2 shows an early attempt at implementing strftime for JavaScript. It’s implemented on Date.prototype which makes it available as a method on all date objects. Don’t despair should you find it hard to understand all the details of the code in this chapter. Concepts are more important than the actual code, and most advanced techniques will be discussed in Part II, JavaScript for Programmers. Listing 1.2 Starting point for strftime for JavaScript Date.prototype.strftime = (function () { function strftime(format) { From the Library of WoweBook.Com Download from www.eBookTM.com ptg 6 Automated Testing var date = this; return (format + "").replace(/%([a-zA-Z])/g, function (m, f) { var formatter = Date.formats && Date.formats[f]; if (typeof formatter == "function") { return formatter.call(Date.formats, date); } else if (typeof formatter == "string") { return date.strftime(formatter); } return f; }); } // Internal helper function zeroPad(num) { return (+num < 10 ? "0" : "") + num; } Date.formats = { // Formatting methods d: function (date) { return zeroPad(date.getDate()); }, m: function (date) { return zeroPad(date.getMonth() + 1); }, y: function (date) { return date.getYear() % 100; }, Y: function (date) { return date.getFullYear(); }, // Format shorthands F: "%Y-%m-%d", D: "%m/%d/%y" }; return strftime; }()); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.1 The Unit Test 7 Date.prototype.strftime mainly consists of two parts: the replace function which takes care of replacing format specifiers with their corresponding values, and the Date.formats object which is a collection of helpers. It can be broken down as follows: • Date.formats is an object with format specifiers as keys and methods to extract the corresponding data from a date as values • Some format specifiers are convenient shortcuts to longer formats • String.prototype.replace is used with a regexp that matches format specifiers • The replacer function checks if a given specifier is available on Date. formats and uses it if it is, otherwise the specifier is left untouched (i.e., returned directly) How would we go about testing this method? One way is to include the script in our web page and use it where we need it and then verify manually if the website displays dates correctly. If it doesn’t work, we probably won’t get a lot of hints as to why, and are left debugging. A slightly more sophisticated approach (although not by much) is to load it in a web page and pop open a console and play around with it. Perhaps something like the session in Listing 1.3. Listing 1.3 Manually checking code in Firebug >>> var date = new Date(2009, 11, 5); >>> date.strftime("%Y"); "2009" >>> date.strftime("%m"); "12" >>> date.strftime("%d"); "05" >>> date.strftime("%y"); "9" Uh-oh. Our Firebug session indicates all is not well with our strftime. This means we’ll have to investigate and rerun the test to verify that it’s working. That’s more manual labor. We can do better. Let’s create a minimal HTML page where we load in the source script along with another script where we add some test code. This way we can inspect the result of changes without having to retype the tests. Listing 1.4 shows the HTML page that we’ll use to test. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 8 Automated Testing Listing 1.4 A HTML test page <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="en"> <head> <title>Date.prototype.strftime test</title> <meta http-equiv="content-type" content="text/html;charset=utf-8"> </head> <body> <script type="text/javascript" src=" /src/strftime.js"> </script> <script type="text/javascript" src="strftime_test.js"> </script> </body> </html> We then copy our console session into a new file, shown in Listing 1.5, which will serve as the test file. To log results we’ll simply use console.log, which is available in most modern browsers, and logs to the browser’s JavaScript console. Listing 1.5 strftime test.js var date = new Date(2009, 11, 5); console.log(date.strftime("%Y")); console.log(date.strftime("%m")); console.log(date.strftime("%d")); console.log(date.strftime("%y")); console.log(date.strftime("%F")); We now have a reproducible test case. We can then attend to the failure: "%y" does not zero pad the number it returns. It turns out we simply forgot to wrap the method call in a zeroPad() call. Listing 1.6 shows the updated Date.formats.y method. Listing 1.6 Zero-pad year Date.formats = { // y: function (date) { return zeroPad(date.getYear() % 100); } // }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.2 Assertions 9 Now we can immediately rerun the test file in a browser and inspect the console to verify that the change fixed the “y” format specifier. In all its simplicity, we’ve now written a unit test. We’re targeting the smallest unit possible in JavaScript—the function. You have probably done something like this many times without being aware of the fact that it is a unit test. While automating the process of creating test objects and calling some methods on them is nice, we still need to manually check which calls are OK and which are not. For a unit test to be truly automated, it needs to be self-checking. 1.2 Assertions At the heart of a unit test is the assertion. An assertion is a predicate that states the programmer’s intended state of a system. When debugging the broken “y” format in the previous section, we carried out a manual assertion: when the strftime method is called on a date from 2009 with the format of "%y", we expect it to return the string "09". If it doesn’t, our system is not working correctly. Assertions are used in unit tests to perform these checks automatically. When an assertion fails, the test is aborted and we’re notified of the failure. Listing 1.7 shows a simple assert function. Listing 1.7 A simple assert function function assert(message, expr) { if (!expr) { throw new Error(message); } assert.count++; return true; } assert.count = 0; The assert function simply checks that its second argument is truthy (i.e., any value except false, null, undefined, 0, "", and NaN). If it is, it incre- ments the assertion counter, otherwise an error is thrown, using the first argument as error message. We can leverage assert in our tests from before, as seen in Listing 1.8. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 10 Automated Testing Listing 1.8 Testing with assert var date = new Date(2009, 9, 2); try { assert("%Y should return full year", date.strftime("%Y") === "2009"); assert("%m should return month", date.strftime("%m") === "10"); assert("%d should return date", date.strftime("%d") === "02"); assert("%y should return year as two digits", date.strftime("%y") === "09"); assert("%F should act as %Y-%m-%d", date.strftime("%F") === "2009-10-02"); console.log(assert.count + " tests OK"); } catch (e) { console.log("Test failed: " + e.message); } This requires slightly more typing, but the test now speaks for itself and is able to verify itself. The manual labor has been reduced from inspecting each and every outcome to simply inspecting the final status reported by the test. 1.2.1 Red and Green In the world of unit testing, “red” and “green” are often used in place of “failure” and “success,” respectively. Having tests go red or green makes the outcome even clearer to interpret, and demands less effort on our part. Listing 1.9 provides a simplified output function which uses the DOM to display messages in color. Listing 1.9 Outputting messages in color function output(text, color) { var p = document.createElement("p"); p.innerHTML = text; p.style.color = color; document.body.appendChild(p); } // console.log can now be replaced with output(assert.count + " tests OK", "#0c0"); // and, for failures: output("Test failed: " + e.message, "#c00"); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.3 Test Functions, Cases, and Suites 11 1.3 Test Functions, Cases, and Suites The test we have built so far has several assertions, but because the assert function throws an error when a test fails, we won’t know whether or not tests following a failing test fail or succeed. For more fine-grained feedback, we can organize our test into test functions. Each test function should exercise only one unit, but it may do so using one or more assertions. For complete control, we can also require each test to only test one specific behavior of a single unit. This means there will be many tests for each function, but they’ll be short and easy to understand, and the test as a whole will provide to-the-point feedback. A set of related test functions/methods is referred to as a test case. In the case of the strftime function, we can imagine a test case for the whole method, with each test testing a specific behavior of the function through one or more assertions. Test cases are usually organized in test suites in more complex systems. Listing 1.10 shows a very simple testCase function. It accepts a string name and an object with test methods. Every property whose name starts with the word “test” is run as a test method. Listing 1.10 A simple testCase function function testCase(name, tests) { assert.count = 0; var successful = 0; var testCount = 0; for (var test in tests) { if (!/^test/.test(test)) { continue; } testCount++; try { tests[test](); output(test, "#0c0"); successful++; } catch (e) { output(test + " failed: " + e.message, "#c00"); } } var color = successful == testCount ? "#0c0" : "#c00"; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 12 Automated Testing output("<strong>" + testCount + " tests, " + (testCount - successful) + " failures</strong>", color); } Listing 1.11 uses testCase to restructure the strftime test into a test case. Listing 1.11 strftime test case var date = new Date(2009, 9, 2); testCase("strftime test", { "test format specifier %Y": function () { assert("%Y should return full year", date.strftime("%Y") === "2009"); }, "test format specifier %m": function () { assert("%m should return month", date.strftime("%m") === "10"); }, "test format specifier %d": function () { assert("%d should return date", date.strftime("%d") === "02"); }, "test format specifier %y": function () { assert("%y should return year as two digits", date.strftime("%y") === "09"); }, "test format shorthand %F": function () { assert("%F should act as %Y-%m-%d", date.strftime("%F") === "2009-10-02"); } }); The tests have so far been distinct and simple enough that we end up with one assertion in each test. The test case now groups all the tests into a single object, but the date object is still being created outside, which is unnatural as it’s an integral part of the test. We could create a new object inside each test, but since we can create it the same way for all of them, that would lead to unnecessary duplication. A better option would be to gather common setup code in a single place. From the Library of WoweBook.Com Download from www.eBookTM.com . their dependencies. We will discuss mocking and stubbing in context in Part III, Real-World Test-Driven Development in JavaScript and in more detail in Chapter 16, Mocking and Stubbing. Having. test</title> <meta http-equiv="content-type" content="text/html;charset=utf-8"> </head> <body> <script type="text /javascript& quot; src=" /src/strftime.js"> </script> <script. digits", date.strftime("%y") === "09"); assert("%F should act as %Y-%m-%d", date.strftime("%F") === "200 9-1 0-0 2"); console.log(assert.count + " tests OK"); }