It’s overwhelmingly easy to write bad unit tests that
add very little value to a project while inflating the cost of code changes
astronomically
TDD helps you to deliver software components that
individually behave according to your design. A suite of good unit tests is
immensely valuable: it documents your design, and makes it easier to refactor
and expand your code while retaining a clear overview of each component’s behavior.
However, a suite of bad unit tests is immensely painful: it doesn’t prove
anything clearly, and can severely inhibit your ability to refactor or alter
your code in any way.
Unit tests created through the TDD process naturally sit at
the extreme left of this scale. They contain a lot of knowledge about the
behavior of a single unit of code. If that unit’s behavior changes, so must its
unit tests, and vice-versa. But they don’t contain any knowledge or
assumptions about other parts of your codebase, so changes to other parts of
your codebase don’t make them start failing (and if yours do, that shows
they aren’t true unit tests). Therefore they’re cheap to maintain, and as a
development technique, TDD scales up to any size of project.
At the other end of the scale, integration tests contain
no knowledge about how your codebase is broken down into units, but instead
make statements about how the whole system behaves towards an external user.
They’re reasonably cheap to maintain (because no matter how you restructure the
internal workings of your system, it needn’t affect an external observer) and
they prove a great deal about what features are actually working today.
Anywhere in between, it’s unclear what assumptions you’re
making and what you’re trying to prove. Refactoring might break these tests, or
it might not, regardless of whether the end-user experience still works.
Changing the external services you use (such as upgrading your database) might
break these tests, or it might not, regardless of whether the end-user
experience still works. Any small change to the internal workings of a single
unit might force you to fix hundreds of seemingly unrelated hybrid tests, so
they tend to consume a huge amount of maintenance time – sometimes in the
region of 10 times longer than you spend maintaining the actual application
code. And it’s frustrating because you know that adding more preconditions to
make these hybrid tests go green doesn’t truly prove anything.
DesignWhen software is developed using a test-driven approach, the unit test may take the place of formal design. Each unit test can be seen as a design element specifying classes, methods, and observable behaviour.
Here is a test class that specifies a number of elements of the implementation. First, that there must be an interface called Adder, and an implementing class with a zero-argument constructor called AdderImpl. It goes on to assert that the Adder interface should have a method called add, with two integer parameters, which returns another integer. It also specifies the behavior of this method for a small range of values.
public class TestAdder {
public void testSum() {
Adder adder = new AdderImpl();
// can it add positive numbers?
assert(adder.add(1, 1) == 2);
assert(adder.add(1, 2) == 3);
assert(adder.add(2, 2) == 4);
// is zero neutral?
assert(adder.add(0, 0) == 0);
// can it add negative numbers?
assert(adder.add(-1, -2) == -3);
// can it add a positive and a negative?
assert(adder.add(-1, 1) == 0);
// how about larger numbers?
assert(adder.add(1234, 988) == 2222);
}
}
In
this case the unit test, having been written first, acts as a design document
specifying the form and behavior of a desired solution, but not the implementation
details, which are left for the programmer. Following the "do the simplest
thing that could possibly work" practice, the easiest solution that will
make the test pass is shown below.
interface Adder {
int add(int a, int b);
}
class AdderImpl implements Adder {
int add(int a, int b) {
return a + b;
}
}
Best Practices: Writing code for a
unit test is as likely to be at least as buggy as the code it is testing. Fred
Brooks in The Mythical Man-Month quotes: never take two chronometers to sea.
Always take one or three. Meaning, if two chronometers contradict, how do you
know which one is correct? Please see the attached slides for best practices to
be followed while writing code for Unit Test.
An example in Java:
http://www.vogella.com/articles/JUnit/article.html
Best Regards:
~RAC
http://www.nickokiss.com/2009/09/unit-testing-presentation.html
No comments:
Post a Comment