TDD – When to Write the Real Code?

tdd

All the examples I've read and seen on training videos have simplistic examples. But what I don't see if how I do the "real" code after I get green. Is this the "Refactor" part?

If I have a fairly complex object with a complex method, and I write my test and the bare minimum to make it pass (after it first fails, Red). When do I go back and write the real code? And how much real code do I write before I retest? I'm guessing that last one is more intuition.

Edit: Thanks to all who answered. All your answers helped me immensely. There seems to be different ideas on what I was asking or confused about, and maybe there is, but what I was asking was, say I have an application for building a school.

In my design, I have an architecture I want to start with, User Stories, so on and so forth. From here, I take those User Stories, and I create a test to test the User Story. The User says, We have people enroll for school and pay registration fees. So, I think of a way to make that fail. In doing so I design a test Class for class X (maybe Student), which will fail. I then create the class "Student." Maybe "School" I do not know.

But, in any case, the TD Design is forcing me to think through the story. If I can make a test fail, I know why it fails, but this presupposes I can make it pass. It is about the designing.

I liken this to thinking about Recursion. Recursion is not a hard concept. It may be harder to actually keep track of it in your head, but in reality, the hardest part is knowing, when the recursion "breaks," when to stop (my opinion, of course.) So I have to think about what stops the Recursion first. It is only an imperfect analogy, and it assumes that each recursive iteration is a "pass." Again, just an opinion.

In implementation, The school is harder to see. Numerical and banking ledgers are "easy" in the sense you can use simple arithmetic. I can see a+b and return 0, etc. In the case of a system of people, I have to think harder on how to implement that. I have the concept of the fail, pass, refactor (mostly because of study and this question.)

What I do not know is based upon lack of experience, in my opinion. I do not know how to fail signing up a new student. I do not know how to fail someone typing in a last name and it being saved to a database. I know how to make a+1 for simple math, but with entities like a person, I don't know if I'm only testing to see if I get back a database unique ID or something else when someone enters a name in a database or both or neither.

Or, maybe this shows I am still confused.

Best Answer

If I have a fairly complex object with a complex method, and I write my test and the bare minimum to make it pass (after it first fails, Red). When do I go back and write the real code? And how much real code do I write before I retest? I'm guessing that last one is more intuition.

You don't "go back" and write "real code". It's all real code. What you do is go back and add another test that forces you to change your code in order to make the new test pass.

As for how much code do you write before you retest? None. You write zero code without a failing test that forces you to write more code.

Notice the pattern?

Let's walk through (another) simple example in hopes that it helps.

Assert.Equal("1", FizzBuzz(1));

Easy peazy.

public String FizzBuzz(int n) {
    return 1.ToString();
}

Not what you would call real code, right? Let's add a test that forces a change.

Assert.Equal("2", FizzBuzz(2));

We could do something silly like if n == 1, but we'll skip to the sane solution.

public String FizzBuzz(int n) {
    return n.ToString();
}

Cool. This will work for all non-FizzBuzz numbers. What's the next input that will force the production code to change?

Assert.Equal("Fizz", FizzBuzz(3));

public String FizzBuzz(int n) {
    if (n == 3)
        return "Fizz";
    return n.ToString();
}

And again. Write a test that won't pass yet.

Assert.Equal("Fizz", FizzBuzz(6));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    return n.ToString();
}

And we now have covered all multiples of three (that aren't also multiples of five, we'll note it and come back).

We've not written a test for "Buzz" yet, so let's write that.

Assert.Equal("Buzz", FizzBuzz(5));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n == 5)
        return "Buzz"
    return n.ToString();
}

And again, we know there's another case we need to handle.

Assert.Equal("Buzz", FizzBuzz(10));

public String FizzBuzz(int n) {
    if (n % 3 == 0)
        return "Fizz";
    if (n % 5 == 0)
        return "Buzz"
    return n.ToString();
}

And now we can handle all multiples of 5 that aren't also multiples of 3.

Up until this point, we've been ignoring the refactoring step, but I see some duplication. Let's clean that up now.

private bool isDivisibleBy(int divisor, int input) {
    return (input % divisor == 0);
}

public String FizzBuzz(int n) {
    if (isDivisibleBy(3, n))
        return "Fizz";
    if (isDivisibleBy(5, n))
        return "Buzz"
    return n.ToString();
}

Cool. Now we've removed the duplication and created a well named function. What's the next test we can write that will force us to change the code? Well, we've been avoiding the case where the number is divisible by both 3 and 5. Let's write it now.

Assert.Equal("FizzBuzz", FizzBuzz(15));

public String FizzBuzz(int n) {
    if (isDivisibleBy(3, n) && isDivisibleBy(5, n))
        return "FizzBuzz";
    if (isDivisibleBy(3, n))
        return "Fizz";
    if (isDivisibleBy(5, n))
        return "Buzz"
    return n.ToString();
}

The tests pass, but we have more duplication. We have options, but I'm going to apply "Extract Local Variable" a few times so that we're refactoring instead of rewriting.

public String FizzBuzz(int n) {

    var isDivisibleBy3 = isDivisibleBy(3, n);
    var isDivisibleBy5 = isDivisibleBy(5, n);

    if ( isDivisibleBy3 && isDivisibleBy5 )
        return "FizzBuzz";
    if ( isDivisibleBy3 )
        return "Fizz";
    if ( isDivisibleBy5 )
        return "Buzz"
    return n.ToString();
}

And we've covered every reasonable input, but what about unreasonable input? What happens if we pass 0 or a negative? Write those test cases.

public String FizzBuzz(int n) {

    if (n < 1)
        throw new InvalidArgException("n must be >= 1);

    var isDivisibleBy3 = isDivisibleBy(3, n);
    var isDivisibleBy5 = isDivisibleBy(5, n);

    if ( isDivisibleBy3 && isDivisibleBy5 )
        return "FizzBuzz";
    if ( isDivisibleBy3 )
        return "Fizz";
    if ( isDivisibleBy5 )
        return "Buzz"
    return n.ToString();
}

Is this starting to look like "real code" yet? More importantly, at what point did it stop being "unreal code" and transition to being "real"? That's something to ponder on...

So, I was able to do this simply by looking for a test that I knew wouldn't pass at each step, but I've had a lot of practice. When I'm at work, things aren't ever this simple and I may not always know what test will force a change. Sometimes I'll write a test and be surprised to see it already passes! I highly recommend that you get in the habit of creating a "Test List" before you get started. This test list should contain all the "interesting" inputs you can think of. You might not use them all and you'll likely add cases as you go, but this list serves as a roadmap. My test list for FizzBuzz would look something like this.

  • Negative
  • Zero
  • One
  • Two
  • Three
  • Four
  • Five
  • Six (non trivial multiple of 3)
  • Nine (3 squared)
  • Ten (non trivial multiple of 5)
  • 15 (multiple of 3 & 5)
  • 30 (non trivial multiple of 3 & 5)
Related Topic