Re: Test Driven Development
From: Phlip (phlip_cpp_at_yahoo.com)
Date: 12/05/03
- Next message: discussion_at_discussion.microsoft.com: "Re: C# Antipatterns?"
- Previous message: Phlip: "Re: The senior architect syndrome"
- In reply to: Uncle Bob (Robert C. Martin): "Re: Test Driven Development"
- Next in thread: Harry Erwin: "Re: Test Driven Development"
- Reply: Harry Erwin: "Re: Test Driven Development"
- Reply: Harry Erwin: "Re: Test Driven Development"
- Reply: Donald Roby: "Re: Test Driven Development"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: Fri, 05 Dec 2003 06:11:49 GMT
Uncle Bob (Robert C. Martin) wrote:
> A good summary, but there's more.
>
> 1. We do not write production code until there is a failing test.
> 2. We write the simplest possible production code to get the test to
> pass.
> 3. We do not write more tests when we have a failing test.
> 4. We do not add to a failing test.
A good summary, but there's more:
When you develop, use a test runner, written in the same language as the
Production Code, that provides some kind of visual feedback at the end of
the test run. Either use a GUI-based test runner that displays a Green Bar
on success or a Red Bar on failure, or a console-based test runner that
displays "All tests passed" or a cascade of diagnostics, respectively.
Write failing tests, and briefly make them pass. Then refactor to improve
maintainability, testing every few edits, to ensure all tested behaviors
stay the same.
Engage each action in this algorithm:
* Locate the next missing CODE ABILITY you want to add
* WRITE A TEST that will only pass if the ability is there
* Run the test and ensure it FAILS FOR THE CORRECT REASON
* Perform the MINIMAL EDIT needed to make the test pass
* When the tests pass and you get a Green Bar, INSPECT THE DESIGN
* While the design (anywhere) is low quality, REFACTOR it
* Only after the design is squeaky clean, PROCEED TO THE NEXT ABILITY.
That algorithm needs more interpretation. Looking closely into each bold
item reveals a field of nuances. All are beholden to this algorithm and to
the intent of each action. Each action leverages different intents; they
often conflict directly with other actions' intents. Our behaviors during
each action differ in opposing ways. Repeated edits with opposing intents
anneal our code in a way that one must experience to fully appreciate and
begin to understand. Suspend disbelief, try the cycle, and report your
results to an Agile Alliance near you.
A CODE ABILITY, in this context, is the current coal face in the mine that
our picks swing at. It's the location in the program where we must add new
behavior, or change existing behavior. Typically, this location is near the
bottom of our most recent function. If we can envision one more line to add
there, or one more edit to make there, then we must perforce be able to
envision the complementing test that will fail without that line or edit.
WRITE A TEST can mean to WRITE A TEST function, and get as far as one
assertion. Alternately, take an existing test function, and add new
assertions to it. To re-use scenarios and bulk up on assertions, we'll
prefer the latter. Some TDD theorists propose minimizing test case size, and
not bulking up on assertions. Expensive setup is a code smell, but if our
GUI Toolkits make setup slow, and provide zillions of side-effects, we make
the most of each test case.
If the new test lines assume facts not in evidence - if, for example, they
reference a class or method name that does not exist yet - run the test
anyway and predict a compiler diagnostic. This test collects valid
information just like any other (and it minimizes all possible reasons not
to hit that test button). If the test inexplicably passes, you may now
understand you were about to write a new class name that conflicted with an
existing one.
Work on the assertion and the code's structure (but not behavior) until the
test FAILS FOR THE CORRECT REASON. If it passes, step thru and inspect the
code to ensure you understand it, and ensure the true reason was indeed
correct; then PROCEED TO THE NEXT FEATURE.
All this work prepares you to make that MINIMAL EDIT. Go ahead and write
that line which you have been anxious to get out of your system for the last
four paragraphs.
The edit is minimal because we live on borrowed time until the Bar turns
Green. Correct behavior and happy tests come (just slightly) before design
quality. We might pass the test by cloning a method and changing one line in
it. If that's the minimum number of edits, do it. Or, re-write a method from
scratch, even if it turns out very similar to an existing method. And often
the simplest edit naturally extends a pre-existing clean design that won't
need refactoring, yet.
The edit is minimal because it may do anything, including lying, to make the
test pass. More tests will force out the lie. Practice this technique, to
develop habits that work even when you don't know the production code lies.
If the MINIMAL EDIT fails, and if the fault is not obvious and simple, just
hit the Undo button and try again. Anything else is preferable to
bug-hunting, and an ounce of prevention is worth a pound of cure.
Now that we have a Green Bar, we INSPECT THE DESIGN. Per the MINIMAL EDIT
principle, the most likely design flaw is duplication. So, to help us learn
to improve things, we spread the definition of "duplication" as wide as
possible, beyond mere code cloning.
The book /Design Patterns/ advises to "abstract the thing that varies". This
is the reverse way to say "merge the duplication that does not vary". So
merging duplication together may tend to approach a Pattern.
To REFACTOR, we inspect our code, and try to envision a design with fewer
moving parts, less duplication, shorter methods, better identifiers,
separated concerns, and deeper abstractions. Start with the code we just
changed, and feel free to involve any other code in the project. But, during
this step, never change functionality - only design.
If we cannot envision a better design, we can proceed to the next step
anyway. Seek MINIMAL EDITs that will either improve the design or lead to a
series of similar edits leading to an improvement. Between each edit, run
all the tests. If any test fails, hit Undo and start again.
Don't REFACTOR in order from hard to easy. Refactor from easy to easy. Start
by picking the low-hanging fruit.
The level of cleanness is important here. You may have code quality that
formerly would have passed as "good enough". Or you may become enamored of
some new abstraction that new code might use, possibly months from now, or
minutes. Snap out of it. The path from cruft to new features is always
harder than the path from elegance to new features. Fix the problems,
including removing any speculative code, while the problems are still small.
If you see duplication, but can't imagine how to improve its design without
obfuscating what it does (or if you can't imagine any way at all), put the
duplicating lines right next to each other. This practice forms little
readable tables.
We may add assertions at nearly any time; while refactoring the design, and
before PROCEEDing TO THE NEXT ABILITY. Whenever we learn something new, or
realize there's something we don't know, we take the opportunity to write
new assertions that express this learning, or query the code's abilities. As
the TDD cycle operates, and individual abilities add up to small features,
we take time to collect information from the code about its current
operating parameters and boundary conditions.
Boundary conditions are the limits between defined behavior and regions
where bugs might live. Set boundaries for a routine well outside the range
you know production code will call it. Research "Design by Contract" to
learn good strategies; these roll defined ranges of behaviors up from the
lower routines to their caller routines. Within a routine, simplifying its
procedure will most often remove discontinuities in its response.
Parameters between these limits now typically cause the code to respond
smoothly with linear variations. The odds of bugs occurring between the
boundaries are typically lower than elsewhere. For example, today's method
that takes 2, 3 and 5 and returns 10, 15 and 25, respectively, is unlikely
tomorrow to take 4 and return 301. Like algebraic substitutions reducing an
expression, duplication removal forces out special cases.
After creating a function, other functions soon call it. Their tests engage
our function too. Our tests cover every statement in a program, and they
approach covering every path in a program. We add features in order of
business values, so the code of highest value - written earliest -
experiences the highest testing pressure and the most test events for the
remaining duration of the project. The cumulative pressure against bugs make
them extraordinarily unlikely.
If you are curious, or code does something unexpected, or you receive a bug
report, always write a new test. Then use what you learned to improve
design, and write more tests of this category. If you treat the situation
"this code does not yet have that ability" as a kind of bug, then the TDD
cycle is nothing but a specialization of the principle "capture bugs with
tests".
Teach your colleagues to write tests on your code, and learn to write tests
on theirs too. If they WRITE A TEST that fails due to missing abilities, not
faults in deployed abilities, treat the failing test as a feature request,
and prioritize it.
-- Phlip
- Next message: discussion_at_discussion.microsoft.com: "Re: C# Antipatterns?"
- Previous message: Phlip: "Re: The senior architect syndrome"
- In reply to: Uncle Bob (Robert C. Martin): "Re: Test Driven Development"
- Next in thread: Harry Erwin: "Re: Test Driven Development"
- Reply: Harry Erwin: "Re: Test Driven Development"
- Reply: Harry Erwin: "Re: Test Driven Development"
- Reply: Donald Roby: "Re: Test Driven Development"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]