I once worked on a project that placed a low priority on process improvement. Everyone complained that building various components of the system took too long, and running regression tests took too long, but no one was willing to change the process. The most common excuse was, "Yes, we know that is a problem, but we are too busy to fix it right now." What amazed those of us working on this project most was the complete lack of value placed on the amount of time it took a developer to complete everyday development tasks.
Compiling and testing are tasks developers perform almost every single day—these tasks should be quick and painless. On this project, however, building just one jar file consisting of a few thousand source files took more than 15 minutes. Running tests involved pulling specific versions of libraries from the CM system, reading directions on multiple Websites, and running five to ten different shell and Perl commands. Once the tests were finished, the developer had to manually decipher test failures hidden in text files with cryptic names. I don't even like to recall how long it took for developers on this project to run the tests.
After a little investigation, the team discovered the build mechanism for that "slow building" library was a poorly written collection of make files. The make files were written such that a separate javac process was forked to compile each source file in the entire library. A simple conversion from the make files to an Ant build file dropped the build time to less than a minute. When we looked into the acceptance tests, we discovered the entire regression test suite consisted of functional tests. By converting the functional tests to unit tests, we were able to cut regression tests that ran for hours down to less than a half hour.
As software engineers, we often deal with performance requirements and end-user acceptance requirements. Why should the development process be any different? Quite frankly, it shouldn't. Development projects should have realistic requirements for how long a build and test cycle should take. Metrics should be collected on a regular basis, so corrective action can be taken as soon as the build and test times exceed requirements.
Selecting the right tools
How does one go about implementing a test-driven development (TDD) process? What do you need to practice TDD? The key to TDD is automating the code-compile-test process with the right set of tools! We've all heard the expression, "Use the right tool for the job." This expression definitely holds true for TDD. Without the right tools, implementing and maintaining a TDD process can be time consuming and painful. It's a standard practice for development teams to use common build scripts or make files for compiling source code. A good TDD process takes this practice a step further by implementing common scripts that combine the compile and test steps into a standard development process. So, what are the right tools?
First and foremost is the test framework itself. A test framework supplies a class hierarchy, convenience APIs, and a common language for writing and running repeatable tests. Development teams would probably end up implementing such a framework if they do not use one that is publicly available. Without such a framework, developers would have to re-create common test harnesses and utility classes just to run their own tests. Depending on the requirements of the application you are building, your test and development process may also require additional framework extensions to cover protocols such as HTTP.
The sample application built throughout the remainder of this book is a J2EE-based Football Pool application for automating your favorite office pool. It must run on multiple operating systems and multiple application servers. For now, you only need to be concerned with how the type of application drives requirements for the TDD process. Since the application is a J2EE application, you know your development and test environment must include a J2SE SDK and a J2EE application server. Adding the application-specific elements to your process requirements of an automated code-compile-test process results in the following TDD requirements:
- A Java test framework supporting common assertions
- A cross-platform scripting tool to start/stop tests and dependent processes
- A cross-platform mechanism for building and packaging Java source code
- An IDE that supports refactoring
- A J2EE application server
- A J2SE SDK
Almost every development project goes through a tool analysis process to determine what tools are needed to develop the software. Unfortunately, spending a few chapters comparing tool choices for each of the preceding requirements would severely limit the amount of TDD material I could cover in this book. The following section lists some recommended tools for implementing the preceding TDD requirements. Each tool is briefly described along with a short explanation of why the tool was chosen for the given requirement. I will mention several alternative approaches related to some of these tools, with a sentence or two on why those approaches weren't used here.
Following the Keep It Simple, Stupid (KISS) and You Aren't Gonna Need It (YAGNI) rules of extreme programming (XP), each tool listed in the following sections fits into the TDD process by meeting one or more of the requirements in its simplest form. The following sections contain a brief description of each tool.
JUnit is the primary test tool for satisfying the first requirement for a Java test framework. JUnit is the Java version of the xUnit architecture for unit- and regression-testing frameworks. Written by Erich Gamma and Kent Beck, it's distributed as an open source project, includes the core test framework class hierarchy, and defines a common language for writing and running repeatable tests. JUnit uses reflection to examine the tests and code under tests. This allows JUnit to execute any method of any class and examine the results. With this framework, all developers on the project know how to write and execute tests, and interpret test results using the following nomenclature:
TestCase: Abstract class for implementing a basic unit test
TestSuite: Composite class for organizing and running groups of tests
Assertions: For testing expected results (
TestRunner: Graphical and text-based test runners
Failure: Indicates a checked test assertion failed (i.e.,
Error: Indicates an unexpected exception or setup failure that stopped the test
JUnit is widely accepted as a standard for unit testing in Java. Many of the available testing products on the market are either based on or extend JUnit. Also, many IDEs currently have built-in support for JUnit. The majority of unit tests written in this book are JUnit tests.
HttpUnit, written by Russell Gold, is an open source Java library for black-box testing HTTP-based Web applications and servlets. The library supports testing applications containing the following:
- Basic and form-based authentication
- State management with cookies
- Automatic page redirection
HttpUnit easily integrates with JUnit for fast development of HTTP-based functional tests. In fact, using HttpUnit with JUnit allows you to write tests for Web applications and entire Websites. HttpUnit provides methods that allow JUnit TestCases to examine returned HTML content as text, an XML DOM (Document Object Model), or containers of forms, tables, and links, as well as provide a way to submit forms, press buttons, and follow links. It also contains a servlet test framework, ServletUnit, which can be used to help develop servlets.
You can meet requirements 2 and 3 (a cross-platform scripting tool to start/stop tests and dependent processes, and a cross-platform mechanism for building and packaging Java source code) by using Ant. Ant is a cross-platform build and scripting tool. It was developed as a Java and XML-based open source project by the Apache Software Foundation. Similar to make, gnumake, and nmake files, Ant build files define project dependencies, classpaths, and build targets. Unlike make, however, Ant is designed for Java with build scripts written as XML files, which can be validated with an XML editor. Also unlike make, Ant dependencies are simple dependencies and are not used to bring targets up to date like make does. Ant is written in Java and can easily be extended to create custom tasks whenever necessary. A default Ant install supports a number of Java-specific tasks (a task being like a command) plus many platform-independent file management tasks for automating the entire build process. Following is a brief list of some of the useful tasks available:
- Compile and run Java
- Create, copy, delete, and move files and directories (with regular expression pattern-matching)
- Create archive files (
- Connect to configuration management systems (CVS, StarTeam, Visual SourceSafe, etc.)
- Run JUnit tests
A key feature of Ant is its integration with the JUnit test framework listed previously. With Ant, you can run multiple sets of JUnit and HttpUnit tests, aggregate the test results, and publish HTML test reports for the entire process to a Web server for immediate viewing by clients or management. Ant scripts control the build and test process for the majority of examples throughout this book.
You could make an argument for using a combination of Perl and make files to control the build process, but I see no reason to introduce two different tools to accomplish what Ant can do by itself. This is a Java project, staffed by Java developers. While Perl is a powerful programming language, and make has been used in software for decades, they are two additional specialized skill sets that are not required for this project. Remember that the development and test environment must be maintained over time just as the product source and test code.
All of the following IDEs meet requirement 4—an IDE that supports refactoring. This is not an exhaustive list, just a list of popular IDEs that you may be familiar with and meet our needs.
While Java development can certainly be accomplished with any text or Java source code editor, maintaining a large base of source code and the accompanying tests is easier to accomplish with an IDE that supports automated refactoring (see Refactoring: Improving the Design of Existing Code, by Martin Fowler et al. [Addison-Wesley, 1999]). One of the common arguments for writing tests is the amount of work required to create and maintain such tests. An IDE that supports refactoring gives developers the necessary tool to accomplish tasks that would otherwise be tedious and time consuming.
Take changing a method signature, for example. Assume that a change in requirements forces a change to the method signature in a common interface. If this method is used in more than 100 lines in various source files, this change could take hours. With a refactoring IDE, this change is completed and tested in just minutes. All three IDEs listed previously have automated features for accomplishing common refactoring techniques such as the following:
- Extract method
- Introduce variable
- Rename/move package
- Extract superclass
- Change method signature
The list of IDEs shown earlier is not intended to be all encompassing. There may be other IDEs on the market that support refactoring. The three listed are either open source or are available as a free trial download.
J2EE application server
Requirement 5 (the application server) is a development, testing, and production requirement. Since the sample application is a J2EE application, it must ultimately run on a J2EE application server. From a unit-testing purist standpoint, the goal for developers should be to write all tests as unit tests, and limit tests that run on the application server to functional tests and end-user acceptance tests.
The sample application should be capable of running on other J2EE application servers with few or no changes.
J2SE Java Development Kit
Requirement 6 is met by any 1.4.x version of the J2SE JDK. It goes without saying that Java development requires a JDK. It is listed here for completeness, and to avoid emails from angry first-time developers complaining that their build script fails to execute.
TDD tool summary
These tools are not absolutely required to practice TDD. The selection of tools does, however, prove that a development team can implement a TDD process in a cost-effective and efficient manner. I would further argue that a typical development team would unnecessarily waste a huge chunk of development time building a fraction of the functionality provided by these tools in any attempt to implement a homegrown TDD solution. Why reinvent the wheel, when there is a perfectly good set of supported products to kick around for FREE?
Setting up your TDD environment
For your convenience and to help speed up the initial setup process, a prepackaged archive of the TDD environment for the sample application is available for download from Apress. For complete instructions on how to download and run the code, see Appendix A. Ant will be the main tool used to build and run the examples, so the next section provides a brief introduction to Ant in case you're not familiar with it.