Tải bản đầy đủ
11 Resources: Ant’s secret data model

11 Resources: Ant’s secret data model

Tải bản đầy đủ

• Ant properties are the key to making builds customizable and controllable. Use
them copiously.
• Remember that properties are almost always immutable. Whoever sets them
first wins.
• Use to define files and directories. Use the
value variant for other string values, including filename fragments if needed.
• Reuse datatype definitions. You should have to declare a path or fileset only once.
• Using to perform simple text substitutions during a build can
accomplish powerful things like inserting dates or other dynamic build-time
information. Be careful not to use it on binary files, however.
• Use conditions and conditional targets and tasks to adapt to the environment.
Build files can be made more robust, or fail with better diagnostics messages.
• Carefully consider the directory structure of your project, including how properties will map to top-level or subordinate directories. By planning this well, a
parent build can easily control where it receives the output of the child build.
• A simple, yet effective, way to manage JAR files in a single project is to place
them all in a subdirectory, usually called lib, and include all these JAR files in
the classpath used to build and run programs.


This chapter has introduced the foundational Ant concepts of paths, filesets, patternsets, filtersets, properties, and references. Let’s look at how they all get used
together. Our compilation step utilizes all of these facilities, either directly or indirectly:


We use a property, build.debug, to control whether compilation is performed
with debug on or off. Typically, the includeAntRuntime value should be set to
no, but our compilation is building a custom Ant task and requires ant.jar. The
task acts as an implicit fileset, with srcdir mapping to ’s
dir attribute. All files in the src tree are considered for compilation because no
excludes or explicit includes were specified. A reference to a previously defined
path, compile.classpath, is used to define our compilation classpath.
From this chapter, several important facts about Ant should stick with you
throughout this book and on into your build file writing:



• Ant uses datatypes to provide rich, reusable parameters to tasks.
is a task that utilizes most of Ant’s datatypes.
• Paths represent an ordered list of files and directories. Many tasks can accept a
classpath, which is an Ant path. Paths can be specified in a cross-platform manner—the MS-DOS conventions of semicolon (;) and backslash (\) or the Unix
conventions of colon (:) and forward slash (/); Ant sorts it all out at runtime.
• Filesets represent a collection of files rooted from a specified directory. Tasks that
operate on sets of files often use Ant’s fileset datatype.
• Patternsets represent a collection of file-matching patterns. Patternsets can be
defined and applied to any number of filesets.
• The actual element names used for datatypes within a task may vary, and a task
may have several different elements all using the same datatype. Some tasks even
implicitly represent a path or fileset. Ant’s documentation clearly defines the types
each attribute and element represents and is the best reference for such details.
• Properties are the heart of Ant’s extensibility and flexibility. They provide a
mechanism to store variables and load them from external resources including
the environment. Unlike Java variables, they’re immutable.
Several additional datatypes have been introduced, but we haven’t provided a lot of
detail yet. We’ll cover them as it’s time to use them; look to chapter 5 in particular.
You already should have a general knowledge of Ant’s abstractions, which will enable
you to define your build process at a high level. Over the next few chapters, we’ll show
you how to do just that.
With Ant’s datatypes introduced and the task thoroughly explored, we
know how to build a simple program. It’s time to start the main project of the book,
which is what the next chapter will do—a diary application and web site. More
importantly, the next chapter will cover writing and running the tests for the main
application, as we’re going to write our code test first!











Testing with JUnit

What is testing, and why do it? 80
Introducing our application 81
How to test a program 83
Introducing JUnit 84
The JUnit task: 93


Generating HTML test reports 99
Advanced techniques 102
Best practices 106
Summary 108

“Any program feature without an automated test simply
doesn’t exist.”
—Kent Beck, Extreme Programming Explained

At this point we’ve learned how Ant can build and run an application. But does the
application work? Does it do what we wanted? Sure, we can use Ant to run the program after the compile, and we can then check the output, but is that adequate?
You can write code, but unless you’re going to write tests or formal proofs of correctness, you have no way of knowing if it works. You can pretend it does, but your
end users will discover the truth. Unless you like fielding support calls, you need to
be testing your code. Ant and JUnit make this possible. JUnit provides the test framework to write your tests in Java, and Ant runs the tests. The two go hand in hand.
JUnit makes it easy to write tests. Ant makes it easy to run those tests, capture the
results, and halt the build if a test fails. It will even create HTML reports.
This chapter is going to look at testing, introduce JUnit, and show you how and
why testing can and should be central to every software project. It will introduce the
program that will be developed through the book. Using this program—a diary—it
will show how Ant can integrate this testing into the build process so that every time
you type ant, the source is compiled and tested. This makes it easy to run the tests
and makes it impossible to ship code that fails those tests.


Testing is running a program or library with valid and invalid inputs to see that it
behaves as expected in all situations. Many people run their application with valid
inputs, but that’s just demonstrating that it can be made to work in controlled circumstances. Testing aims to break the application with bad data and to show that it’s
broken. Automated testing is the idea that tests should be executed automatically, and
the results should be evaluated by machines. Modern software development processes
all embrace testing as early as possible in the development lifecycle. Why?
To show that code works
With a test, you can pass in parameters that are designed to break the program, to
stress it at the corners, and then you can see what happens. A well-written test often
reveals bugs in your code that you can fix.
To replicate bugs
If you can write a unit test that replicates the bug, you are one step closer to fixing it.
You have a way of verifying the problem, and you have some code you can step into to
find out what’s going wrong. The best part? The test remains, stopping the bug from
creeping back.
To avoid proofs-of-correctness
In theory, formal methods can let you prove that a piece of code works. In practice,
they can’t. The complexity of a modern system implies that you need to have studied
something like the pi-calculus to stand a chance. Even if you have the skills, are you
really going to prove everything still works after every change?
We believe formal logic has a place in software. However, we also think that the
people writing the java.util.concurrent libraries should do the proofs, not us.
To test on different platforms
If you’ve automated tests, you can run them on all target platforms. That includes Java
versions, application/web server releases, and operating systems. People always criticize
Java as “write once, test everywhere,” but once testing becomes easy, testing everywhere
becomes possible. At that point, you really do have code that runs everywhere.
To enable regression testing
A new Java release comes out every six to twelve months. Libraries in a big project
may be updated every month or two. How do you know that your program still works
whenever a piece is upgraded? Regression testing, that’s how. Regression tests verify
that an application still works the way it used to, that there have been no regressions.
All bug-replication tests become regression tests the moment the bug is fixed.




To enable refactoring
Refactoring is now a well-known concept: the practice of rearranging your code to
keep it clean and to help it adapt to change. As defined by Martin Fowler, refactoring
is “the restructuring of software by applying a series of internal changes that do not
affect its observable behavior” (Fowler 1999). That is, it changes the internals of a
program without changing what it does.
If you’re going to refactor your program, large portions of your source can change
as they’re moved around, restructured, or otherwise refactored—often at the click of
a button in the IDE. After you make those changes, how can you be sure that everything is still working as before? The only way to know is through those tests that you
wrote before you began refactoring, the tests that used to work.
Automated testing transforms how you develop programs. Instead of writing code
and hoping that it works, or playing with a program by hand to reassure yourself that
all is well, testing can show how much of a program really is working. Ant goes hand
in hand with this concept, because it integrates testing with the build process. If it’s
easy to write tests and easy to run them, there’s no longer any reason to avoid testing.


This is the first place in our book where we delve into the application that we built to
accompany this text. We’re going to use this application through most of the remaining chapters.
Why is the “testing” chapter the right place to introduce our application?
Because the tests were written alongside our application: the application didn’t exist
until this chapter.


The application: a diary
We’re going to write a diary application that will store appointments somewhere and
print them out. Later on, the diary will save data to a database and generate RSS feeds
and HTML pages from that database. We’ll add these features as we go along, extending the application, the tests, and the build file in the process.
Using an agile process doesn’t mean we can skip the design phase. We just
avoid overdesigning before implementing anything we don’t immediately need.
Accordingly, the first step for the application is to sketch out the architecture in a
UML tool. Figure 4.1 shows the UML design of the library.
The core of our application will be the Events class, which will store Event
instances. Every Event must have a non-null id, a date, and a name; extra text
is optional. The operation Event.equals() compares only the ID values; the
hashCode() value is also derived from that. The Event.compareTo operator is
required to have the same semantics as the equals operator, so it too works only on
the ID value. To sort events by time, we must have a special Comparator implementation, the DateOrder class. We mark our Event as Serializable to use