Code Contracts - TDD in a DbC World
Lately, I've been talking about the new feature coming to .NET 4.0, Code Contracts, which is to bring Design by Contract (DbC) idioms to all .NET languages as part of the base class library. Last week, I attended QCon, where Greg Young, one of my CodeBetter cohorts, gave a talk titled "TDD in a DbC World" in which he talked about that they are not in conflict, but instead are complementary to a test first mentality. Both improve upon the usage of the other.
One of the questions that came up during some of my blog posts and even at QCon for that matter. Many times, DbC gets pegged as being in conflict with TDD, and that many people who espouse the use of DbC aren't doing TDD, and I don't think that's necessarily true. Let's look at where each fits into the general picture.
Where Does Design by Contract Fit?
The first thing we need to remember about DbC is that it is specified to create constraints through the use of contracts. This gives us the ability to specify acceptable input, state constraints of invariants, and output that can be guaranteed. Not only to specify this, but also statically verify this behavior through the compiler, as well as at run time. That's the most important part is the proving. Think of it as as a nun from Catholic school who will come up behind you and rap your knuckles should you violate the contract, which is a constant reminder that you are not living up to your end of the deal.
DbC, when applied properly, serves as documentation. The edge cases of your tests can then melt away as you realize what is and is not acceptable input, state and output from the API. At this point, many of those test cases on constraints for input, output and state could be eliminated due to it being superfluous with our contracts in place. Looking at the following code using Spec# to write these preconditions, what's the value of this test?
public void Add(Employee employee)
requires employee != null otherwise ArgumentNullException;
{
// Adds employee here
}
[Fact]
public void AddWithNullEmployee_ShouldThrowException()
{
// Assert
Assert.Throws<ArgumentNullException>(
() => employees.Add(null));
}
The question then comes up, what are we actually creating these tests for anyway? Edge cases and constraints, or actual business value? What I'm not advocating is deleting this test, but de-emphasizing its role using DbC constructs and to utilize the compilers warnings as guides. This test can also serve as documentation going forward.
Thinking About BDD
What we need to think about when it comes to TDD, when done properly is testing behavior of our system. This is where the real business value is, and not in the constraints, and the edge cases, so writing tests for those constraints doesn't make sense. Maybe then it's better to talk about this as Behavior Driven Development (BDD) because ultimately, that's what our tests are flushing out with our design and leave the word Test Driven Development at the door as the name really doesn't fit.
What we need to test is behavior and the rich interactions that take place, which is something that DbC can't necessarily ensure. As well, the contracts can't verify the business value of said test either. The rule should be, DbC for your constraints, and TDD for the business value.
Now, how does it fit together? Well, let's walk through a hyperbole of a a situation to see what is DbC and what is TDD/BDD. For example, let's take the example of an application that launches a nuclear missile and destroys some land mass. What in there is DbC and what is TDD?
public TargetResult LaunchMissile(Target target)
requires Target != null otherwise ArgumentNullException;
ensures Button.IsPressed;
{
// Some logic to destroy civilization
}
What we've defined for DbC is our constraints for our given functionality under test. Then what might TDD be in this situation? Well, let's look at the two major ways of thought around the issue of being either a classicist or a mockist in terms of our testing style.
[Fact]
public void LaunchMissile_ShouldDestroyTarget()
{
var target = // Sets up target
var missile = new FakeMissile();
var result = launcher.LaunchMissile(target);
Assert.True(missle.WasLaunched);
Assert.True(result.IsDestroyed);
}
// Mockist
[Fact]
public void LaunchMissile_ShouldDestroyTarget()
{
var target = // Sets up target
var missile = new Mock<IMissile>();
missile.Expect(x => x.SetTarget(target));
missile.Expect(x => x.Launch());
var result = launcher.LaunchMissile(target);
missile.VerifyAll();
}
Let's move forward with the discussion towards some general ways of tackling the issues described here.
Moving Forward
The question may arise how to tell the difference between a constraint and a behavior? Here's one example that might make it more clear. If an integer that is passed into my functionality under test is out of range and exceptional, then it is a constraint. If, however, that an integer out of range is ok because another rule might apply instead, then it is behavior.
Moving forward with development, how do we do it? As I mentioned before, we have two options here:
- Define contract first for API level designs for well known behaviors, then define tests, and then finally implement the logic to get the test to pass.
- Define tests first, implement the logic, get to pass, then refactor constraints to the contract.
For most situations, we'll go with the second option and then over time, refine and then narrow the contracts. Then if we can, we can generalize these contracts through interfaces to define constraints that apply to all implementations.
Conclusion
As developers start to discover the upcoming Code Contracts for .NET 4.0, it's important to understand not only Design by Contract, and TDD, but how they can complement each other. For business value, we as developers need to flush out our design through the use of TDD, and we can refactor into using contracts for our constraints over time. With these methodologies in place, the edge cases will melt away and help us ensure DRY and write more concise code directly having business value.
Once again, pick up the latest version of the Code Contracts for .NET and give the team feedback.