Clinton D. Judy wrote:
I’d like someone to give me very basic reasons for why I need Behavior
Driven Development (BDD, see RSpec) and/or Test Driven Development (TDD,
See Test::Unit).
Shine the “Phlip” symbol on the underside of the nearest cloud deck!
I’m a fairly competent programmer. I think I use what they call an
“iterative” style of programming; I don’t rush out to build the entire
program at once. I do a very small framework, then attempt to run it and
see if it’s giving me the result I expect. If it’s not, then I figure
out what’s going wrong, and attempt to solve it before going on to the
next small part. You could say I do several iterations in an hour, to
give an idea of time.
When you say “attempt to run it”, you should be mixing manual testing
with
running unit tests. They give a second opinion.
However, they are permanent. Imagine if you put a “mini-me” in a bottle,
who
came out and manually tested, instantly, each time you changed the code.
Imagine
if it would warn if nearly any behavior changed since the last run.
You could go faster - making bigger changes - with less manual testing.
It looks like TDD does exactly what I’m doing, but introduces far more
code to the equation.
Yet that code should be super-easy to write. A TDD test should obey the
Assemble
Activate Assert pattern:
def test_case
foo = assemble_foo()
result = foo.activate()
assert{ result == 42 }
end
Compared to production code, that test code looks super-easy to write.
Tip: The
easier a test is to write, the cleaner and more decoupled your code is.
So,
counter-intuitively, the more lazy you get writing tests, the more
pressure
forces your code to decouple.
To do TDD, you write the test case first, then write code to pass the
test. You
only write new code if you have a failing test case. So your code is
decoupled
before it is even written.
One of the biggest features of TDD is being able
to run a suite of tests to make sure new features don’t break old
features. Yet I have an eye for this while developing; I either fix the
upcoming problem because I know it’s going to happen, or I’m doing it
wrong in the first place. Sure, I occasionally miss, but who doesn’t? I
find the issue as quickly as I can and resolve it in a clean style.
You should run a TDD test suite after the fewest possible edits: 10 at
the most,
and hopefully just 1. A coding session should go type-type Test,
type-type Test,
type-type Test, in tiny cycles.
If you delay to run the test, the more risk you absorb with your edits.
This helps sustain projects as they get huge; the code stays just as
easy to
work with as a tiny project. Nobody can cross-check everything in a huge
program.
BDD looks like a way to make it easy for non-programmers to do
programming, but not having people keep several functions in mind at any
one time. And it also does TDD? Am I close?
Yes - BDD is TDD with an added layer: A “literate programming” framework
that
forces you to think in clear English* statements that your client could
understand.
*or whatever your client speaks!
That anyone can do TDD with BDD (and they do) speaks volumes about
Ruby’s
ability to support easy and flexible DSLs.
But everyone continually praises TDD and BDD, which leads me to believe
I’m missing something profound that could make me a better programmer.
So what’s the big deal?
The big deals are: Almost no debugging, a super-high velocity, the
ability to
deploy any integration, the ability to rapidly share code with
colleagues, code
that strongly resists new bugs, and the ability to allow your client to
“steer”
your project, feature-by-feature, in real-time.
If your tests fail unexpectedly, the fault must lie in your last edit.
You can
undo or revert it. If your tests gate your Subversion or Gitorious
version
controller, then you know that you can always revert your code to get
rid of a
test failure. Working in tiny cycles, and constantly integrating your
code,
allows you to take bigger steps with more confidence.
Debugging is the greatest time-waster in all programming. Grizzled
senior
programmers (like some of my friends!) can all remember working on
projects with
bugs so big, you could spend the first week just isolating which module
contained the bug. With debugging out of the way, you spend your
remaining time
understanding requirements, implementing them, integrating them, and
deploying
them. Your project goes very fast as your client learns to request very
small
features, each an increment over the existing features. Very few
features go to
waste. And a project with a complete test rig cannot lose its value over
time.
New code is often easier to write than old code, even despite projects
with
thousands of features. This metric is unheard of in software
engineering, where
new features are typically harder to write than the first ones.
If your tests gate your integrations, than you could deploy any
integration. It
might have unfinished features (and you might make their View buttons
invisible!), but the tests will tell you it has no bugs. This metric is
also
unheard-of in classical programming, where a project typically must
endure a
“polish phase”, as much as 25% of development time, to get it “ready to
ship”.
Next, if I need my colleagues to be able to mess with code I wrote (and
vice
versa), then I don’t need them breaking it because they didn’t
understand it
(what are the odds?:). If they run my tests (and they do!), then there’s
a
little bit of me in them, working with them, keeping my features stable
while
they add theirs.
The TDD cycle has three steps: Write a test that fails for the correct
reason,
write simple code that passes the test, and refactor your code to make
it clean
and dry. Notice you don’t refactor until the code passes its test.
Refactoring
means merging the new code with the preexisting code. And you don’t
refactor
until your tests say the new code works. This implies only good code
with valid
features gets merged together. TDD’s influence on design cannot be
understated:
It produces rock-solid code that strongly resists bugs.
Under classic software development, you would ask your client for a long
list of
features, then ask to be left alone for a while! Under TDD, your new
features
can be reviewed as they emerge. Your client has better odds of getting
the
features they want if they practice “just in time requirements”. They
only
require features in small increments from those installed features which
they
like. TDD allows a business cycle as short as a week to add features,
review the
code, and request for features. This improves the odds your client gets
the
features they need - not just some of the features they ask for. Your
client no
longer has the burden of predicting the future and planning every
feature they
might need.
The Ruby on Rails project has copious unit tests; its success is an
example of
these forces at work…