jaywayco - Lost in IT Reverie

jaywayco

jconway
by jconway

Common Sense Vs TDD

A couple of days ago I heard someone say about TDD that a line of production code is not as important as a line of test code.

What?!

This really started alarm bells ringing. How can production code ever be deemed to be less important than anything else? Production code is production code! It's code that runs in production!

Again, I'll try to open my mind to this a bit...

Make It Fail

So in TDD, the aim is not to write any production code without writing a test for it first. This I can accept (although I find it weird). We write a test and it MUST NOT pass, in the start it probably won't build. Then we write the minimum amount of code to make the test pass. Once it passes we re-factor our code so that it is as efficient and as readable as possible. We then make sure our test still passes and then we write a new test for extra functionality. This cycle is often called "red, green re-factor".

You can see how this lends itself to production code being second class. Production code exists only to make our tests pass.

YAGNI

Another phrase that is thrown around a lot with TDD is "you ain't gonna need it" or "YAGNI". That is, never write code that is only to serve a theoretical future requirement. Only write the minimum amount of code to pass the tests.

YAGNI I can partially get on board with. However, my motivation is different.

Sometimes it is favourable to get a feature into the production environment without a beautiful extensible framework underpinning it. We may have two features that we are A/B split testing and until we know which feature we want to run with, it makes no sense wasting time writing beautiful code because it may be thrown out. There is a down side to this though, it takes a lot of discipline to make the time to re-architect the chosen feature. The same is true with the TDD motivated YAGNI ethos.

Back to "red, green, re-factor though". I can possibly get on board with this if and only if the requirements are perfect up front.

Requirements, Requirements, Requirements...

We all know that requirements are never perfect. Clients will always be clients and ask for things in a way they don't really mean it. Humans will always be humans and cannot communicate perfectly in every situation. So this can cause problems. In TDD we have to take requirements literally and use these requirements to write our tests. If the client has missed something from the requirements or assumed that the developer would "use common sense" then this is where functionality would be incorrect.

Is this a bad thing though? Does this not give us traceability? Does this not give us a blame tool so when somebody starts shouting about an incomplete feature we can prove that it was the requirements that were incomplete rather than the implementation?

Lets use an example to try to answer this question.

Roman Numeral Translator

A good example I have come across to demonstrate TDD is a Roman Numeral translator. Lets say we have the following requirement written as a user story.

As a reader of roman texts I need to have a roman numeral translator so that I can quickly and easily translate roman numerals to modern digits.

Details

I need a translator that can translate roman numerals into numbers. For example I need the roman numeral XV translated to 15 or the roman numeral VIII to translate to 8.

This seems like a decent requirement we have a clearly defined requirement with some details. But is it as clearly defined as we might think?

If we do our Noun/Verb analysis on this story we can see that these will be our objects:

  • Translator
  • Roman numeral
  • Number

And the following methods:

  • Translate

We will need to implement a translator and it is here that most of our functionality is likely to end up. Our Roman numerals will be strings. Our numbers will be integers. It also makes sense to have the translate method on the Translator. Ok, so lets write our first test:

[Test]
public void Initialize_Translator()
{
    var translator = new Translator();
    Assert.IsNotNull(translator);
}

This wont build let alone pass. So lets write some code to make this pass:

public class Translator
{
}

If we run our test again we will see that the test passes. Now we can add a new test to add some more functionality. From our user story we can see that we need to translate roman numerals into numbers and one of the examples given is translating "XV" into 15. So lets write our test:

[Test]
public void Translate_XV_to_integer_15()
{
    var translator = new Translator();
    Assert.IsNotNull(translator);
    var fifteen = translator.Translate("XV");
    Assert.AreEqual(fifteen, 15);
}

Again this wont build. But this is good, so lets write some code to make this pass:

public class Translator
{
    public int Translate(string romanNumeral)
    {
        return 15;
    }
}

If we run our tests again this will pass. Excellent! Back to our user story. We have been given another example to work with so lets write a test for that use case:

[Test]
public void Translate_VIII_to_integer_8()
{
    var translator = new Translator();
    Assert.IsNotNull(translator);
    var eight = translator.Translate("VIII");
    Assert.AreEqual(eight, 8);
}

This will build but fail. SO lets re-factor our code to make this test pass:

public class Translator
{
    public int Translate(string romanNumeral)
    {
        if(romanNumeral == "VIII")
        {
            return 8;
        }
        return 15;
    }
}

If we run our tests again they should all pass. But hang on. This code is obviously ridiculous. Ok it covers the use cases but it is very obviously not fit for purpose. So off we pop back to our client and explain that the story needs to be expanded. They have only given two examples and we have used our common sense to assume that something must have been missed. The client then updates the story to be:

As a reader of roman texts i need to have a roman numeral translator so that i can quickly and easily translate roman numerals to modern digits.

Details

I need a translator that can translate roman numerals into numbers. For example i need the roman numeral XV translated to 15 or the roman numeral VIII to translate to 8.

Update:

The translator should translate all possible combinations of the following roman numerals:

Roman Numeral Equivalent Digit
I 1
V 5
X 10
L 50
C 100
D 500
M 1000

See Wikipedia for full details.

Er, All combinations? This has turned complicated somewhat. However, this approach has highlight the problem with the requirement. It has proven that the story was not detailed enough for us to develop a full solution or rather the solution that was actually required.

Now we know what the client actually required we can get to work. What should be my new test then? Well a test for each numeral in the table would be a good start and relatively easy. Also we have enough to go on for input validation type tests. But after that the functionality gets complicated and the possible combinations (without getting into a philosophical mathematical debate) could be infinite. In my opinion this is where TDD stops being useful and assumptions have to be made about potential usages. More importantly though, this is the point where production code becomes a first class citizen. We must write our code so that it is very general and we have a high confidence that if a few cases work then all will. TDD doesn't help us do this.

So TDD enforces that we need to write all of our tests up front. We are not allowed to write code that we think we might need. So we need to try to cover every single combination? Without any forward planning? This is where I think TDD falls down. I'm not saying this solution would be impossible with TDD but I think it will be made much harder and more sluggish. The number of tests we need to cover this feature will be very high. In TDD that means a lot of refactoring. Probably a re-factor after each new test passed.

In a non TDD environment we can stand back and think about what problems we might encounter. We can think about the problem as a whole and design our solution without writing a line of code. We can then implement a solution to our problem without needing to worry about writing code that doesn't function first. Then we can write our tests which will be just as numerous and cover the same functionality but the tests will stay out of the way of design. And to me that is key.

Conclusion

So I think I am managing to persuade myself that TDD is useful to an extent. It can be used to validate user stories and basic functionality. However, we must be careful not to put up a wall of ignorance in these situations. We should not be saying "You asked for X you got X" in situations when we can apply common sense to see that a requirement is lacking. This attitude will create bad feeling and introduce the old Tech/Business divide. Which will only cause trouble.

In scenarios like the example above, whether we are doing TDD or not we end up in the same position. We cannot pre-empt every use case of our system. Assumptions have to be made about how our code will handle input that isn't tested.

But the idea that production code is somewhat less important than tests, for me, impossible to accept. If we take that attitude we could end up with very poorly designed code that looks like the example translator above. While I agree this is an extreme example it highlights the point that treating production code like it is a means to an end could leave you in a position where we have a very brittle design. We rely on being able to re-architect code written like this which is always easier said than done.

I can accept TDD is a useful tool but design and maintainability have to be at the forefront.

Share the joy
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
If you like this blog follow me on twitter @jaywaycon or Google+




Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>