Write the smallest amount of code to pass your test.

When introduced to Test-Driven Development (TDD), frequently you will read or hear that you should write only the smallest amount of production code to get your failing test to pass. There are several reasons for doing this: We don’t want to over-engineer or complicate our system, we want it to be simple to understand and maintain, and most importantly we don’t want to have any code that is not covered by a test. That’s all fine and good, but how do I know that I’m actually writing the bare minimum necessary?

Let’s start with a simple problem in order to convey the message I’m trying to describe:

Babysitter Kata

Background
----------
This kata simulates a babysitter working and getting paid for one night.
The rules are pretty straight forward:

The babysitter
- starts no earlier than 5:00PM
- leaves no later than 4:00AM
- gets paid $12/hour from start-time to bedtime
- gets paid $8/hour from bedtime to midnight
- gets paid $16/hour from midnight to end of job
- gets paid for full hours (no fractional hours)

Feature:
As a babysitter
In order to get paid for 1 night of work
I want to calculate my nightly charge

Source: https://gist.github.com/jameskbride/5482722

After reviewing the requirements we decide that the first test we should write should test that working a single hour before bedtime should calculate the babysitter’s pay to be $12 for the night. We create and call a calculatePay method that takes the start time, end time, and bedtime. Let’s just assume a 24 hour clock for the time being.

import org.junit.Test;

import static org.junit.Assert.*;

public class BabysitterTest {

    @Test
    public void testWhenTheBabysitterWorksForOneHourBeforeBedtimeThenTheBabysitterReceives12Dollars() {
        Babysitter babysitter = new Babysitter();
        int actualPayment = babysitter.calculatePay(17, 18, 20);
        assertEquals(12, actualPayment);
    }
}

After this test is written, we run the test and get some compilation errors that our Babysitter class doesn’t exist. We create that class, then we create our calculatePay method after the compiler complains about that being missing too. We return 0 inside this method to simply satisfy its return type. At this point we should see our test truly fail with the following production code:

public class Babysitter {

    public int calculatePay(int startTime, int endTime, int bedTime) {
        return 0;
    }
}

java.lang.AssertionError:
Expected :12
Actual :0

Cool. At this point you may be tempted to do some math because this problem is easy to solve. Maybe a little (endTime — startTime) * 12 will do the trick. It will certainly get this test to pass. I mean, how could that be bad? It means that you’re writing production code that is not covered by a test. With a small example like this, it’s pretty easy to get a good idea of what needs to be done, making it tempting to cheat or jump ahead. With larger systems containing dozens or even hundreds of classes, this becomes even more important when you can’t reasonably keep track of everything that’s going on.

The smallest thing we can do to get our first test to pass is to simply return 12:

public class Babysitter {

    public int calculatePay(int startTime, int endTime, int bedTime) {
        return 12;
    }
}

Our test passes with this. Whenever you have a failing test, you should always challenge yourself to write the smallest amount of production code to get that test to pass, no matter how ridiculous or useless you think the production code is. Trust your tests. Your tests will tell you if you have a particular piece of functionality implemented or not. In the end you will notice that your code seems smaller, cleaner, and more purposeful. Better yet, the next developer that comes along to add functionality to this or maintain it probably won’t hate you.

Now we can move on and write a test for multiple hours before bedtime. This will enable us to write out some of the math we may have wanted to write during the first test.

@Test
public void testWhenTheBabysitterWorksForTwoHoursBeforeBedtimeThenTheBabysitterReceives24Dollars() {
    Babysitter babysitter = new Babysitter();
    int actualPayment = babysitter.calculatePay(17, 19, 20);
    assertEquals(24, actualPayment);
}

Now that we see this test fails because our method is always returning 12, we change our production code to the following to get our second test to pass:

public class Babysitter {

    public int calculatePay(int startTime, int endTime, int bedTime) {
        return (endTime - startTime) * 12;
    }
}

You may start to wonder why my tests specify some variables passed into the method that are unused at the moment. Technically for my first test I could have had a calculatePay method with no arguments. The only reason I did this was to give my tests more meaning. TDD doesn’t necessarily mean that you give no thought to a little upfront design. The kata specifies that I need to know a start time, end time and a bedtime, so I don’t need to make things more difficult on myself by trying to avoid certain things in the beginning.

How do I know I wrote the bare minimum necessary to make the test pass?

With a trivial example like this babysitter kata, it can be easy to recognize that the easiest/smallest thing I can do is to return 12. But what about something a little less trivial? Maybe you’re writing tests for banking software or the space shuttle. Your tests might be larger, and a test may require you to add a significant amount of code. Here are a few things you can keep in mind to recognize that you may be straying from the path:

1. You write a test that you expect to fail, but it instantly passes.
This is likely an indication that you wrote too much production code to get a previous test to pass. This is an after-the-fact situation, but if you start to recognize this as soon as it occurs and make an attempt to understand why, you will run into this situation far less in the future. My recommendation for when you reach this point is to fix up the production code you wrote previously in order to get only this new test to fail. You may be removing a chunk of functionality that you thought was important, but it’s just as important to make sure you see your test fail before you make it pass. Otherwise you run the risk of having unnecessary production code.

2. Your test coverage tool indicates that your production code coverage dipped below 100%.
If you’re writing tests in any capacity, I would recommend using a test coverage tool if possible (EclEmma for Eclipse is a good example). A new project started with TDD should begin with and maintain a test coverage percentage of 100%. If you’re on a project where this isn’t the case, just at least make sure that you’re not decreasing the test coverage percentage. Sometimes you’ll be on a project with multiple teams and not every team will be as diligent with adding tests (if they even write tests). It’s important that you do your best to keep the quality at its highest. If your test coverage tool finds that a conditional branch you just added to your code is only covered for one case and not the other, it’s a good indication that you added the condition too early before a test dictated that it was necessary. Try removing the condition completely and see if your test still passes, then drive that back in with a second test.

3. You’re throwing more code at the problem.
You just wrote some production code to get your test to pass. Easy! Now you run your tests and find out that the test is still failing. Darn. Let’s just add another line or two of code here. Crap, the test is still failing. Let’s add a few more lines of code. The test still won’t pass? Now you have a pile of garbage that isn’t working and you’re confused because it seems like it should be. If you find yourself to be in this situation, you need to take a step back and return to what is most simple. Double-check that your test has been written properly and undo your production changes. If I know I can get my test to pass by simply returning 12, but when I do that I’m still getting 0 back from that method, it can point me to a completely different location. This happened to me once when I neglected to initialize memory for the babysitter object I was testing. Oops! In this case, no amount of production code would help me.

Sometimes it’s hard to write such a small amount of code because you already have a vision in your head of the solution. I know that my calculatePay method will not always just give me $12, but it’s important not to cheat and skip ahead. Challenge yourself to keep your production code as small, clean and maintainable as possible. Trust your tests and you’ll go far.