ptg 1.3 Test Functions, Cases, and Suites 13 1.3.1 Setup and Teardown xUnit frameworks usually provide setUp and tearDown methods. These are called before and after each test method respectively, and allow for centralized setup of test data, also known as test fixtures. Let’s add the date object as a test fixture using the setUp method. Listing 1.12 shows the augmented testCase function that checks if the test case has setUp and tearDown, and if so, runs them at the appropriate times. Listing 1.12 Implementing setUp and tearDown in testCase function testCase(name, tests) { assert.count = 0; var successful = 0; var testCount = 0; var hasSetup = typeof tests.setUp == "function"; var hasTeardown = typeof tests.tearDown == "function"; for (var test in tests) { if (!/^test/.test(test)) { continue; } testCount++; try { if (hasSetup) { tests.setUp(); } tests[test](); output(test, "#0c0"); if (hasTeardown) { tests.tearDown(); } // If the tearDown method throws an error, it is // considered a test failure, so we don't count // success until all methods have run successfully successful++; } catch (e) { output(test + " failed: " + e.message, "#c00"); } } From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14 Automated Testing var color = successful == testCount ? "#0c0" : "#c00"; output("<strong>" + testCount + " tests, " + (testCount - successful) + " failures</strong>", color); } Using the new setUp method, we can add an object property to hold the test fixture, as shown in Listing 1.13 Listing 1.13 Using setUp in the strftime test case testCase("strftime test", { setUp: function () { this.date = new Date(2009, 9, 2, 22, 14, 45); }, "test format specifier Y": function () { assert("%Y should return full year", this.date.strftime("%Y") == 2009); }, // }); 1.4 Integration Tests Consider a car manufacturer assembly line. Unit testing corresponds to verifying each individual part of the car: the steering wheel, wheels, electric windows, and so on. Integration testing corresponds to verifying that the resulting car works as a whole, or that smaller groups of units behave as expected, e.g., making sure the wheels turn when the steering wheel is rotated. Integration tests test the sum of its parts. Ideally those parts are unit tested and known to work correctly in isolation. Although high-level integration tests may require more capable tools, such as software to automate the browser, it is quite possible to write many kinds of integra- tion tests using a xUnit framework. In its simplest form, an integration test is a test that exercises two or more individual components. In fact, the simplest integration tests are so close to unit tests that they are often mistaken for unit tests. In Listing 1.6 we fixed the “y” format specifier by zero padding the re- sult of calling date.getYear(). This means that we passed a unit test for Date.prototype.strftime by correcting Date.formats.y. Had the lat- ter been a private/inner helper function, it would have been an implementation From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.4 Integration Tests 15 detail of strftime, which would make that function the correct entry point to test the behavior. However, because Date.formats.y is a publicly available method, it should be considered a unit in its own right, which means that the afore- mentioned test probably should have exercised it directly. To make this distinction clearer, Listing 1.14 adds another format method, j, which calculates the day of the year for a given date. Listing 1.14 Calculating the day of the year Date.formats = { // j: function (date) { var jan1 = new Date(date.getFullYear(), 0, 1); var diff = date.getTime() - jan1.getTime(); // 86400000 == 60 * 60 * 24 * 1000 return Math.ceil(diff / 86400000); }, // }; The Date.formats.j method is slightly more complicated than the previous formatting methods. How should we test it? Writing a test that asserts on the result of new Date().strftime("%j") would hardly constitute a unit test for Date.formats.j. In fact, following the previous definition of integration tests, this sure looks like one: we’re testing both the strftime method as well as the specific formatting. A better approach is to test the format specifiers directly, and then test the replacing logic of strftime in isolation. Listing 1.15 shows the tests targeting the methods they’re intended to test directly, avoiding the “accidental integration test.” Listing 1.15 Testing format specifiers directly testCase("strftime test", { setUp: function () { this.date = new Date(2009, 9, 2, 22, 14, 45); }, "test format specifier %Y": function () { assert("%Y should return full year", Date.formats.Y(this.date) === 2009); }, From the Library of WoweBook.Com Download from www.eBookTM.com ptg 16 Automated Testing "test format specifier %m": function () { assert("%m should return month", Date.formats.m(this.date) === "10"); }, "test format specifier %d": function () { assert("%d should return date", Date.formats.d(this.date) === "02"); }, "test format specifier %y": function () { assert("%y should return year as two digits", Date.formats.y(this.date) === "09"); }, "test format shorthand %F": function () { assert("%F should be shortcut for %Y-%m-%d", Date.formats.F === "%Y-%m-%d"); } }); 1.5 Benefits of Unit Tests Writing tests is an investment. The most common objection to unit testing is that it takes too much time. Of course testing your application takes time. But the alternative to automated testing is usually not to avoid testing your application completely. In the absence of tests, developers are left with a manual testing process, which is highly inefficient: we write the same throwaway tests over and over again, and we rarely rigorously test our code unless it’s shown to not work, or we otherwise expect it to have defects. Automated testing allows us to write a test once and run it as many times as we wish. 1.5.1 Regression Testing Sometimes we make mistakes in our code. Those mistakes might lead to bugs that sometimes find their way into production. Even worse, sometimes we fix a bug but later have that same bug creep back out in production. Regression testing helps us avoid this. By “trapping” a bug in a test, our test suite will notify us if the bug ever makes a reappearance. Because automated tests are automated and reproducible, we can run all our tests prior to pushing code into production to make sure that past mistakes stay in the past. As a system grows in size and complexity, manual regression testing quickly turns into an impossible feat. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.5 Benefits of Unit Tests 17 1.5.2 Refactoring To refactor code is to change its implementation while leaving its behavior intact. As with unit tests, you have likely done it whether you called it refactoring or not. If you ever extracted a helper method from one method to reuse it in other methods, you have done refactoring. Renaming objects and functions is refactoring. Refactoring is vital to growing your application while preserving a good design, keeping it DRY (Don’t Repeat Yourself) and being apt to adopt changing requirements. The failure points in refactoring are many. If you’re renaming a method, you need to be sure all references to that method have changed. If you’re copy-pasting some code from a method into a shared helper, you need to pay attention to such details as any local variables used in the original implementation. In his book Refactoring: Improving the Design of Existing Code [1], Martin Fowler describes the first step while refactoring the following way: “Build a solid set of tests for the section of code to be changed.” Without tests you have no reliable metric that can tell you whether or not the refactoring was successful, and that new bugs weren’t introduced. In the undying words of Hamlet D’Arcy, “don’t touch anything that doesn’t have coverage. Otherwise, you’re not refactoring; you’re just changing shit.”[2] 1.5.3 Cross-Browser Testing As web developers we develop code that is expected to run on a vast combination of platforms and user agents. Leveraging unit tests, we can greatly reduce the required effort to verify that our code works in different environments. Take our example of the strftime method. Testing it the ad hoc way involves firing up a bunch of browsers, visiting a web page that uses the method and manually verifying that the dates are displayed correctly. If we want to test closer to the code in question, we might bring up the browser console as we did in Section 1.1, The Unit Test, and perform some tests on the fly. Testing strftime using unit tests simply requires us to run the unit test we already wrote in all the target environments. Given a clever test runner with a bunch of user agents readily awaiting our tests, this might be as simple as issuing a single command in a shell or hitting a button in our integrated development environment (IDE). 1.5.4 Other Benefits Well-written tests serve as good documentation of the underlying interfaces. Short and focused unit tests can help new developers quickly get to know the system being From the Library of WoweBook.Com Download from www.eBookTM.com ptg 18 Automated Testing developed by perusing the tests. This point is reinforced by the fact that unit tests also help us write cleaner interfaces, because the tests force us to use the interfaces as we write them, providing us with shorter feedback loops. As we’ll see in Chapter 2, The Test-Driven Development Process, one of the strongest benefits of unit tests is their use as a design tool. 1.6 Pitfalls of Unit Testing Writing unit tests is not always easy. In particular, writing good unit tests takes practice, and can be challenging. The benefits listed in Section 1.5, Benefits of Unit Tests all assume that unit tests are implemented following best practices. If you write bad unit tests, you might find that you gain none of the benefits, and instead are stuck with a bunch of tests that are time-consuming and hard to maintain. In order to write truly great unit tests, the code you’re testing needs to be testable. If you ever find yourself retrofitting a test suite onto an existing application that was not written with testing in mind, you’ll invariably discover that parts of the application will be challenging, if not impossible, to test. As it turns out, testing units in isolation helps expose too tightly coupled code and promotes separation of concerns. Throughout this book I will show you, through examples, characteristics of testable code and good unit tests that allow you to harness the benefits of unit testing and test-driven development. 1.7 Summary In this chapter we have seen the similarities between some of the ad hoc testing we perform in browser consoles and structured, reproducible unit tests. We’ve gotten to know the most important parts of the xUnit testing frameworks: test cases, test methods, assertions, test fixtures, and how to run them through a test runner. We implemented a crude proof of concept xUnit framework to test the initial attempt at a strftime implementation for JavaScript. Integration tests were also dealt with briefly in this chapter, specifically how we can realize them using said xUnit frameworks. We also looked into how integration tests and unit tests often can get mixed up, and how we usually can tell them apart by looking at whether or not they test isolated components of the application. When looking at benefits of unit testing we see how unit testing is an investment, how tests save us time in the long run, and how they help execute regression tests. Additionally, refactoring is hard, if not impossible, to do reliably without tests. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 1.7 Summary 19 Writing tests before refactoring greatly reduces the risk, and those same tests can make cross-browser testing considerably easier. In Chapter 2, The Test-Driven Development Process, we’ll continue our explo- ration of unit tests. We’ll focus on benefits not discussed in this chapter: unit tests as a design tool, and using unit tests as the primary driver for writing new code. From the Library of WoweBook.Com Download from www.eBookTM.com ptg This page intentionally left blank From the Library of WoweBook.Com Download from www.eBookTM.com ptg 2 The Test-Driven Development Process I n Chapter 1, Automated Testing, we were introduced to the unit test, and learned how it can help reduce the number of defects, catch regressions, and increase de- veloper productivity by reducing the need to manually test and tinker with code. In this chapter we are going to turn our focus from testing to specification as we delve into test-driven development. Test-driven development (TDD) is a programming technique that moves unit tests to the front row, making them the primary entry point to production code. In test-driven development tests are written as specifica- tion before writing production code. This practice has a host of benefits, including better testability, cleaner interfaces, and improved developer confidence. 2.1 Goal and Purpose of Test-Driven Development In his book, Test-Driven Development By Example[3], Kent Beck states that the goal of test-driven development is Clean code that works. TDD is an iterative develop- ment process in which each iteration starts by writing a test that forms a part of the specification we are implementing. The short iterations allow for more instant feedback on the code we are writing, and bad design decisions are easier to catch. By writing the tests before any production code, good unit test coverage comes with the territory, but that is merely a welcome side effect. 21 From the Library of WoweBook.Com Download from www.eBookTM.com ptg 22 The Test-Driven Development Process 2.1.1 Turning Development Upside-Down In traditional programming problems are solved by programming until a concept is fully represented in code. Ideally, the code follows some overall architectural design considerations, although in many cases, perhaps especiallyin the worldof JavaScript, this is not the case. This style of programming solves problems by guessing at what code is required to solve them, a strategy that can easily lead to bloated and tightly coupled solutions. If there are no unit tests as well, solutions produced with this approach may even contain code that is never executed, such as error handling logic, and edge cases may not have been thoroughly tested, if tested at all. Test-driven development turns thedevelopment cycle upside-down.Rather than focusing on what code is required to solve a problem, test-driven development starts by defining the goal. Unit tests form both the specification and documentation for what actions are supported and accounted for. Granted, the goal of TDD is not testing and so there is no guarantee that it handles edge cases better. However, because each line of code is tested by a representative piece of sample code, TDD is likely to produce less excessive code, and the functionality that is accounted for is likely to be more robust. Proper test-driven development ensures that a system will never contain code that is not being executed. 2.1.2 Design in Test-Driven Development In test-driven development there is no “Big Design Up Front,” but do not mistake that for “no design up front.” In order to write clean code that is able to scale across the duration of a project and its lifetime beyond, we need to have a plan. TDD will not automatically make great designs appear out of nowhere, but it will help evolve designs as we go. By relying on unit tests, the TDD process focuses heavily on individual components in isolation. This focus goes a long way in helping to write decoupled code, honor the single responsibility principle, and to avoid unnecessary bloat. The tight control over the development process provided by TDD allows for many design decisions to be deferred until they are actually needed. This makes it easier to cope with changing requirements, because we rarely design features that are not needed after all, or never needed as initially expected. Test-driven development also forces us to deal with design. Anytime a new feature is up for addition, we start by formulating a reasonable use case in the form of a unit test. Writing the unit test requires a mental exercise—we must describe the problem we are trying to solve. Only when we have done that can we actually start coding. In other words, TDD requires us to think about the results before providing the solution. We will investigate what kind of benefits we can reap from this process From the Library of WoweBook.Com Download from www.eBookTM.com . %F": function () { assert("%F should be shortcut for %Y-%m-%d", Date.formats.F === "%Y-%m-%d"); } }); 1 .5 Benefits of Unit Tests Writing tests is an investment. The most. rotated. Integration tests test the sum of its parts. Ideally those parts are unit tested and known to work correctly in isolation. Although high-level integration tests may require more capable. risk, and those same tests can make cross-browser testing considerably easier. In Chapter 2, The Test-Driven Development Process, we’ll continue our explo- ration of unit tests. We’ll focus on