Test-driven design (TDD) (Beck
2003;
Astels
2003), is an
evolutionary approach to development which combines test-first development where
you write a test before you write just enough production code to fulfill that
test and
refactoring.
What is the primary goal of TDD? One view is the goal of TDD is specification and not validation (Martin,
Newkirk, and Kess 2003). In other words, it’s one way to think through your design
before your write your functional code. Another
view is that TDD is a programming technique. As Ron Jeffries likes to say, the goal of TDD is to write clean code that
works. I think that there is merit
in both arguments, although I lean towards the specification view, but I leave it for you to decide.
|
|
Table of Contents
- What is TDD?
- TDD and traditional testing
- TDD and documentation
- Test-driven database development
- Scaling TDD via Agile Model-Driven Development (AMDD)
- Why TDD?
- Myths and misconceptions
- Summary
- Tools
The
steps of test first design (TFD) are overviewed in the UML activity diagram of Figure 1.
The
first step is to quickly add a test, basically just enough code to fail.
Next you run your tests, often the complete test suite although for sake of
speed you may decide to run only a subset, to ensure that the new test does in
fact fail. You then update your functional code to make it pass the new
tests. The fourth step is to run your tests again. If they fail you
need to update your functional code and retest. Once the tests pass the
next step is to start over (you may first need to refactor any duplication out of
your design as needed, turning TFD into TDD).
Figure
1. The Steps of test-first design (TFD).
I like to describe TDD with this simple formula:
TDD = Refactoring + TFD.
TDD completely turns traditional development around.
When you first go to implement a new feature, the first question that you ask is
whether the existing design is the best design possible that enables you to
implement that functionality. If so, you proceed via a TFD approach.
If not, you refactor it locally to change the portion of the design affected by
the new feature, enabling you to add that feature as easy as possible. As
a result you will always be improving the quality of your design, thereby making
it easier to work with in the future.
Instead of writing functional code first and then your testing code as an
afterthought, if you write it at all, you instead write your test code before
your functional code. Furthermore, you do so in very small steps – one test and a
small bit of corresponding functional code at a time. A programmer taking a TDD approach refuses to write a new
function until there is first a test that fails because that function isn’t
present. In fact, they refuse to
add even a single line of code until a test exists for it. Once the test is in place they then do the work required to ensure that
the test suite now passes (your new code may break several existing tests as
well as the new one). This sounds simple in principle, but when you are first
learning to take a TDD approach it proves require great discipline because it is
easy to “slip” and write functional code without first writing a new test. One of the advantages of
pair programming
is
that your pair helps you to stay on track.
An underlying assumption of TDD is that you have a unit-testing
framework available to you. Agile
software developers often use the xUnit family of open source tools, such as JUnit
or VBUnit, although commercial tools are
also viable options. Without such
tools TDD is virtually impossible.
Figure
2 presents a UML state chart diagram for how people typically work with the xUnit tools. This diagram was suggested to me by
Keith Ray.
Figure 2. Testing via the xUnit
Framework.
Kent Beck, who popularized TDD in eXtreme Programming (XP)
(Beck
2000), defines two simple rules for TDD (Beck
2003). First, you should write new business code only when an automated test has
failed. Second, you should
eliminate any duplication that you find. Beck
explains how these two simple rules generate complex individual and group
behavior:
-
You design organically, with the running code providing
feedback between decisions.
-
You write your own tests because you can't wait 20
times per day for someone else to write them for you.
-
Your development environment must provide rapid
response to small changes (e.g you need a fast compiler and regression test
suite).
-
Your designs must consist of highly cohesive, loosely
coupled components (e.g. your design is highly normalized) to make testing
easier (this also makes evolution and maintenance of your system easier
too).
For developers, the implication is that they need to learn
how to write effective unit tests. Beck’s
experience is that good unit tests:
-
Run fast (they have short setups, run times, and break
downs).
-
Run in isolation (you should be able to reorder them).
-
Use data that makes them easy to read and to
understand.
-
Use real data (e.g. copies of production data) when
they need to.
-
Represent one step towards your overall goal.
TDD is primarily a design technique with a side effect
of ensuring that your source code is thoroughly unit tested. However, there is more to testing than this. You'll still need to consider other testing techniques such as
agile acceptance testing and investigative testing. Much of this testing can also
be done early in your project if you choose to do so (and you should). In fact, in XP the acceptance tests for a user story are specified by the project stakeholder(s) either before or in
parallel to the code being written, giving stakeholders the confidence that the
system does in fact meet their requirements.
With traditional testing a successful test finds one or
more defects. It is the same with
TDD; when a test fails you have made progress because you now know that you need
to resolve the problem. More
importantly, you have a clear measure of success when the test no longer fails.
TDD increases your confidence that your system actually meets the requirements
defined for it, that your system actually works and therefore you can proceed
with confidence.
As
with traditional testing, the greater the risk profile of the system the more
thorough your tests need to be. With both traditional testing and TDD you aren't
striving for perfection, instead you are testing to the importance of the
system. To paraphrase Agile
Modeling (AM), you should "test with a purpose" and know why you
are testing something and to what level it needs to be tested.
An interesting side effect of TDD is that you achieve 100% coverage test
– every single line of code is tested – something that traditional testing
doesn’t guarantee (although it does recommend it). In
general I think it’s fairly safe to say that although TDD is a specification
technique, a valuable side effect is that it results in significantly
better code testing than do traditional techniques.
|
If it's worth building, it's
worth testing.
If it's not worth testing, why
are you wasting your time working on it?
|
|
Like it or not most programmers don’t read the written
documentation for a system, instead they prefer to work with the code.
And there’s nothing wrong with this.
When trying to understand a class or operation most programmers will
first look for sample code that already invokes it.
Well-written unit tests do exactly this – the provide a working
specification of your functional code – and as a result unit tests effectively
become a significant portion of your technical documentation. The implication is
that the expectations of the pro-documentation crowd need to reflect this
reality.
Similarly, acceptance tests can
form an important part of your requirements documentation.
This makes a lot of sense when you stop and think about it.
Your acceptance tests define exactly what your stakeholders expect of
your system, therefore they specify your critical requirements.
Your regression test suite, particularly with a test-first approach, effectively
becomes detailed
executable specifications.
Are tests sufficient documentation? Very likely not, but they do form an important part of it.
For example, you are likely to find that you still need user, system
overview, operations, and support documentation.
You may even find that you require summary documentation overviewing the
business process that your system supports.
When you approach documentation with an open mind, I suspect that you
will find that these two types of tests cover the majority of your documentation
needs for developers and business stakeholders. Furthermore, they are a
wonderful example of AM's
Single
Source Information practice and an important part of your overall efforts to
remain as
agile as possible regarding documentation.
At
the time of this writing an important question being asked within the agile
community is “can TDD work for data-oriented development?” When you look at the process depicted in Figure
1 it is important to note that none of the steps specify object programming
languages, such as Java or C#, even though those are the environments TDD is
typically used in. Why couldn't you
write a test before making a change to your database schema?
Why couldn't you make the change, run the tests, and refactor your schema
as required? It seems to me that
you only need to choose to work this way.
My
guess is that in the near term database TDD, or perhaps Test Driven Database
Design (TDDD), won't work as smoothly as
application TDD. The first
challenge is tool support. Although
unit-testing tools, such as DBUnit,
are
now available they are still an emerging technology at the time of this
writing.
Some
DBAs are improving the quality of the testing they doing, but I haven’t
yet
seen anyone take a TDD approach to database development. One challenge
is that unit testing tools are still not well accepted within the data
community, although that is changing, so my expectation is that over
the next
few years database TDD will grow. Second,
the concept of evolutionary development is new to many data professionals and as
a result the motivation to take a TDD approach has yet to take hold.
This issue affects the nature of the tools available to data
professionals – because a
serial mindset still dominates within the
traditional data community most tools do not support evolutionary development.
My hope is that tool vendors will catch on to this shift in paradigm, but
my expectation is that we'll need to develop open source tools instead.
Third, my experience is that most people who do data-oriented work seem
to prefer a model-driven, and not a test-driven approach.
One cause of this is likely because a test-driven approach hasn't been
widely considered until now, another reason might be that many data
professionals are likely visual thinkers and therefore prefer a modeling-driven
approach.
TDD is very good at detailed specification and validation,
but not so good at thinking through bigger issues such as the overall design,
how people will use the system, or the UI design (for example). Modeling,
or more to the point agile model-driven
development (AMDD) (the lifecycle for which is captured in
Figure 3) is better
suited for this. AMDD addresses the scaling issues which TDD does not.
Figure 3. The Agile Model Driven
Development (AMDD) lifecycle.
Comparing TDD and AMDD:
-
TDD shortens the programming feedback loop whereas AMDD
shortens the modeling feedback loop.
-
TDD provides detailed specification (tests) whereas
AMDD is better for thinking through bigger issues.
-
TDD promotes the development of high-quality code
whereas AMDD promotes high-quality communication with your stakeholders and
other developers.
-
TDD provides concrete evidence that your software works
whereas AMDD supports your team, including stakeholders, in working toward a
common understanding.
-
TDD “speaks” to programmers whereas AMDD speaks to
business analysts, stakeholders, and data professionals.
-
TDD is provides very finely grained concrete feedback
on the order of minutes whereas AMDD enables verbal feedback on the order
minutes (concrete feedback requires developers to follow the practice Prove
It With Code and thus becomes dependent on non-AM techniques).
-
TDD helps to ensure that your design is clean by
focusing on creation of operations that are callable and testable whereas
AMDD provides an opportunity to think through larger design/architectural
issues before you code.
-
TDD is non-visually oriented whereas AMDD is visually
oriented.
-
Both techniques are new to traditional developers and
therefore may be threatening to them.
-
Both techniques support evolutionary development.
Which approach should you take? The answer depends on your, and your teammates, cognitive preferences. Some people are primarily "visual thinkers", also called
spatial thinkers, and they may prefer to think things through via drawing. Other people are primarily text oriented, non-visual or non-spatial
thinkers, who don't work well with drawings and therefore they may prefer a TDD
approach. Of course most people
land somewhere in the middle of these two extremes and as a result they prefer
to use each technique when it makes the most sense. In short,
the answer is to use the two techniques together so as to gain the advantages of
both.
How do you combine the two
approaches? AMDD should be used to
create models with your project stakeholders to help explore their requirements
and then to explore those requirements sufficiently in architectural and design
models (often simple sketches). TDD
should be used as a critical part of your build efforts to ensure that you
develop clean, working code. The
end result is that you will have a high-quality, working system that meets the
actual needs of your project stakeholders.
A significant advantage of TDD is that it enables you to take
small steps when writing software. This
is a practice that I have promoted for years because it is far more productive
than attempting to code in large steps. For
example, assume you add some new functional code, compile, and test it. Chances are pretty good that your tests will be broken by defects that
exist in the new code. It is much
easier to find, and then fix, those defects if you've written two new lines of
code than two thousand. The implication is that the faster your compiler and
regression test suite, the more attractive it is to proceed in smaller and
smaller steps. I generally prefer
to add a few new lines of functional code, typically less than ten, before I
recompile and rerun my tests.
I think
Bob Martin says it well “The act of writing a unit test
is more an act of design than of verification. It is also more an act of documentation than of verification. The act of writing a unit test closes a remarkable number of feedback
loops, the least of which is the one pertaining to verification of function”.
The first reaction that many people have to agile techniques is
that they're ok for small projects, perhaps involving a handful of people for
several months, but that they wouldn't work for "real" projects that
are much larger. That’s
simply not true. Beck (2003)
reports working on a Smalltalk system taking a completely test-driven approach
which took 4 years and 40 person years of effort, resulting in 250,000 lines of
functional code and 250,000 lines of test code. There are 4000 tests running in under 20 minutes, with the full suite
being run several times a day. Although
there are larger systems out there, I've personally worked on systems where
several hundred person years of effort were involved, it is clear that TDD works
for good-sized systems.
There are several common myths and misconceptions which people
have regarding TDD which I would like to clear up if possible. Table 1 lists these myths and describes the reality.
Table 1. Addressing the myths and
misconceptions surrounding TDD.
Myth |
Reality |
You create a 100% regression test suite |
Although this sounds like a good goal, and it is, it
unfortunately isn't realistic for several reasons:
- I may have some reusable
components/frameworks/... which I've downloaded or purchased which
do not come with a test suite, nor perhaps even with source code.
Although I can, and often do, create black-box tests which validate
the interface of the component these tests won't completely validate
the component.
- The user interface is really hard to test.
Although user interface testing tools do in fact exist, not everyone
owns them and sometimes they are difficult to use. A common
strategy is to not automate user interface testing but instead to
hope that
user testing efforts cover this important aspect of your system.
Not an ideal approach, but still a common one.
- Some developers on the team may not have adequate
testing skills.
-
Database regression testing is a fairly new concept and not yet
well supported by tools.
- I may be working on a
legacy system and may not yet have gotten around to writing the
tests for some of the legacy functionality.
|
The unit tests form 100% of your design specification |
People new to agile software development, or
people claiming to be agile
but who really aren't, or perhaps people who have never been involved
with an actual agile project, will sometimes say this. The reality
is that the unit test form a fair bit of the
design
specification, similarly
acceptance tests form a fair bit of your
requirements specification, but there's more to it than this.
As Figure 3 indicates, agilists do in fact
model (and
document for that matter), it's just that we're very smart about how
we do it. Because you think about the production code before you
write it, you effectively perform detailed design as I highly
suggest reading my
Single Source Information: An Agile Practice for Effective Documentation
article. |
You only need to unit test |
For all but the simplest systems this is completely
false. The agile community is very clear about the need for
a host of
other testing
techniques. |
TDD is sufficient for testing |
TDD, at the unit/developer test as well as at the
customer test level, is only part of your overall testing efforts.
At best it comprises your confirmatory testing efforts, but as
Figure 4 shows you must also be concerned about
investigative testing efforts which go beyond this. See
Agile Testing
Strategies for details. |
TDD doesn't scale |
This is partly true, although easy to overcome.
Scalability issues include:
- Your test suite takes too long to run.
This is a common problem with a equally common solutions.
First, separate your test suite into two components. One test
suite contains the tests for the new functionality that you're
currently working on, the other test suite contains all tests.
You run the first test suite regularly, migrating older tests for
mature portions of your production code to the overall test suite as
appropriate. The overall test suite is run in the background,
often on a separate machine(s), and/or at night. Second, throw
some hardware at the problem.
- Not all developers know how to test.
That's often true, so get them some appropriate training and get
them pairing with people with unit testing skills. Anybody who
complains about this issue more often than not seems to be looking
for an excuse not to adopt TDD.
- Everyone might not be taking a TDD approach.
Taking a TDD approach to development is something that everyone on
the team needs to agree to do. If some people aren't doing so,
then in order of preference: they either need to start, they need to
be motivated to leave the team, or your team should give up on TDD.
|
Figure 4. Agile testing.
Test-driven design (TDD) is a development technique where you must
first write a test that fails before you write new functional code. TDD is being quickly adopted by agile software developers for development
of application source code and is even being adopted by Agile DBAs for
database
development. TDD should be seen as complementary to Agile
Model Driven Development (AMDD) approaches and the two can and should be
used together. TDD does not replace
traditional testing, instead it defines a proven way to ensure effective unit
testing. A side effect of TDD is
that the resulting tests are working examples for invoking the code, thereby
providing a working specification for the code. My experience is that TDD works
incredibly well in practice and it is something that all software
developers should consider adopting.
The following is a representative list of TDD tools available to you.
Please email me
with suggestions. I also maintain a list of
agile database development
tools.
10. References and Suggested Online Readings