08 September 2018

TDD, Declarative Code, Deleting Tests, an Important Nuance

I've had something stuck in my craw for about a year now. Some debates and discussions of TDD led me to this condition that I was stuck for an articulation on. I think I got it now and I want to share it.

The Core of TDD

TDD is about driving out a solution from tests. That is to say, rule 1, tests are the cause of code in production. The goal is to drive out a solution. It should be minimalist, it should be clean, and it should only do what the test say it should. This is well understood.

Deleting Tests

One topic that seems to come up a lot is deleting tests. When do you do it? Why do you do it? 

As I've said before, I generally don't delete tests unless they are wrong. That isn't to say that I never do, but I don't spend much time worrying about it. I've built plenty of large systems with thousands of test and never had a problem caused by too many tests. It just isn't a thing. 

What I am likely to do is replace a test. That is, if I see a particularly bad example of a test -- I might kill it off and replace it with one or more cleaner, more concise tests. 

Communicating Intent Through Tests

Tests have a secondary function. Communication. When I want to understand the authors intent, I can look at the tests and understand what the author was driving at; in a well tested system at least. I don't always have to find the author, or track down the story card, or have a protracted and speculative conversation about what the author intended. I can read the tests and understand what is supposed to be happening. Admittedly I might lack the Why, but What is half the battle.

So, when we talk about deleting tests, I feel like that is an affront to a secondary characteristic of TDD. It offends me. We are deleting the expression of What the code under tests should do; how it behaves.

Declarative Code and Tests

Anyway, this leads me to the bit that has been bugging me. Deleting tests of declarative code. Or tests for 'obvious code'. The argument that 'The code explains itself and the tests don't add value' does not resonate with me. Follow along here, see if this make sense to you.

If the tests are the What
And the code only satisfies that What
And I delete code, I can recover the What by looking at the test.

If the tests are the What
And I delete the tests
I don't know What is supposed to be happening.

It is hubris to presume that we are smart enough to know or infer what the authors intent was. Further, it is wasteful -- why speculate when we can know?

An Example Case

The common occurrance of this debate topic lately seems to be around declarative code. Take this example;

class RuleApplier {

  private static final Map<Class, List<Rule>> ruleMap = new HashMap<>();

  static {
    /* code that loads up all the various possible rules goes here */
    ruleMap.put(BlueThingy.class, Arrays.asList(new BlueThingRule1(), new GenericThingRule()));
  }

  public static final void applyRules(Thingy thingy) {
    for (Rule rule : ruleMap.get(thingy.getKey())) {
      thingy.apply(rule);
    }
  }
}

Should there be a test that indicates that ruleMap should contain a rule for BlueThingy? Should it be specific about what rules are applied? 

I'd say yes!

You howl, Why? Thats silly? Thats testing structure and implementation? 

I submit the following defense.

In Defense of Not Deleting

If I test drove the solution I would have started with a test that says something like;

public class RuleApplierTest {

  @Test
  public void blueThingyGetsBlueThingRuleAndGenericThingRuleApplied() {
    BlueThingy thingy = mock(BlueThingy.class);
    when(thingy.getKey()).thenCallRealMethod();
    InOrder ruleApplications = inOrder(thingy);

    RuleApplier.applyRules(thingy);

    ruleApplications.verify(thingy).apply(isA(BlueThingRule1.class));
    ruleApplications.verify(thingy).apply(isA(GenericThingRule.class));
  }

}

There is nothing about that test that says RuleApplier has a declarative block of code, or even that it needs one? It only says RuleApplier will apply those two rules when given a BlueThingy.

The Nuance

If I delete this test 'because it is testing declarative code' I'm removing the what. So later, when I wonder about why, I won't have the benefit of a what to clue me in. In fact, I can now easily break the system. And surely you agree, relying on an integrated test is a bad choice; integrated tests are a sham.

There, I finally got that out of my system.