TDD: What About Code You Need But Have No Test For Yet?

26 07 2015

I spend a lot of time teaching TDD to other developers.  Here’s a situation I run into a lot.

Given an opening test of this sort:

@Test
public void shouldReturnChangeIfPaymentIsSufficient () {
    int price = 1999;
    int payment = 2000;

    int result = subject.conductTransaction (price, payment);

    assertEquals (1, result);
}

my mentee will begin supporting it like this:

public int conductTransaction (int price, int payment) {
    if (price >= payment) {

and I’ll stop him immediately.

“Whoa,” I’ll observe, “we don’t have a test for that yet.”

“Huh?” he says.

“We never write a line of production code that’s not demanded into existence by a failing test,” I’ll say. “That if statement you’re writing will have two sides, and we only have a test for one of those sides. Before we have that other test, we can’t write any code that steps outside the tests we do have.”

So I’ll erase his if statement and proceed like this:

public int conductTransaction (int price, int payment) {
    return price - payment;
}

We run the test, the test passes, and now we can write another test:

@Test
public void shouldThrowExceptionIfPaymentIsInsufficient () {
    int price = 1999;
    int payment = 1998;

    try {
        subject.conductTransaction (price, payment);
        fail ();
    }
    catch (IllegalArgumentException e) {
        assertEquals ("Payment of $19.98 is insufficient to cover $19.99 charge");
    }
}

Now we have justification to put that if statement in the production code.

Frequently, though, my mentee will be unsatisfied with this. “What if we get distracted and forget to add that second test?” he’ll ask. “We’ll have code that passes all its tests, but that is still incorrect and will probably fail silently.”

Until recently, the only response I could come up with was, “Well, that’s discipline, isn’t it? Are you a professional developer, or are you a hobbyist who forgets things?”

But that’s not really acceptable, because I’ve been a developer for four and a half decades now, and while I started out doing a pretty good job of forgetting things, I’m getting better and better at it as more of my hair turns gray.

So I started teaching a different path. Instead of leaving out the if statement until you (hopefully) get around to putting in a test for it, put it in right at the beginning, but instrument it so that it complains if you use it in an untested way:

public int conductTransaction (int price, int payment) {
    if (price < payment) {
        throw new UnsupportedOperationException ("Test-drive me!");
    }
    return price - payment;
}

Now you can wait as long as you like to put in that missing test, and the production code will remember for you that it’s missing, and will throw you an exception if you exercise it in a way that doesn’t have a test.

That’s better.  Much better.

But there are still problems.

First, “throw new UnsupportedOperationException ("Test-drive me!");” takes longer to type than I’d like.

Second, unless you want to string-search your code for UnsupportedOperationExceptions, and carefully ignore the ones that are legitimate but not the ones that are calling for tests (error-prone), there’s no easy way to make sure you’ve remembered to write all the tests you need.

So now I go a step further.

Somewhere in most projects, in the production tree, is at least one Utils class that’s essentially just an uninstantiable bag of class methods.  In that class (or in one I create, if that class doesn’t exist yet) I put a method like this:

   public void TEST_DRIVE_ME () {
        throw new UnsupportedOperationException ("Test-drive me!");
    }

Now, whenever I put in an if statement that has only one branch tested, I put a TEST_DRIVE_ME() call in the other branch.  Whenever I have to create an empty method so that a test will compile, instead of having it return null or 0 or false, I put a TEST_DRIVE_ME() in it.

Of course, in Java you still have to return something from a non-void method, but it’s just compiler candy, because it’ll never execute after a TEST_DRIVE_ME(). Some languages are different; for example, in Scala you can have TEST_DRIVE_ME() return Nothing—which is a subclass of every type—instead of being void, which makes things even easier.

Are you about to complain that I’m putting test code in the production tree, and that that’s forbidden?

Okay, fine, but wait to complain just a little longer, until after the next paragraph.

The really cool part is that when you think you’re done with your application, and you’re ready to release it, you go back to that bag-of-methods Utils class and just delete the TEST_DRIVE_ME() method. Now the compiler will unerringly find all your untested code and complain about it–whereupon you can Ctrl-Z the TEST_DRIVE_ME() back in and go finish your application, and then delete it again when you’re really done.

See? No test code in the production tree!


Actions

Information

Leave a comment