Friday 14 February 2014

Fake it till you make it


tldr; fake it till you make it is an underused approach to TDD, but when you can get it to work it gives you the best experience. See Kent Becks book TDD:By Example for a more thorough explanation (this book is highly recommended if you want to understand TDD).

Green bar patterns

In Kent Becks book, TDD: By Example he outlines three approaches to get the tests passing. There's Obvious Implementation, where you just write to code to pass the test. This method works well, but where the implementation is anything more than trivial, it can be difficult to write the correct code. Another is Triangulation, where you create multiple tests to force yourself to write to the implementation. Triangulation is the least recommended by Kent Beck of all patterns because you end up with a lot of redundant tests. Unfortunately, it is probably the most used.

There's a third approach though which I find gives me the best results (when I can get it right) and that's called Fake It (Till You Make It). The idea with Fake It is to hard code the expected result to get to green straight away. Then remove duplication between the test and the implementation.

Using the word wrap example.

I'm going to work through this with the word wrap kata, which is:
Break a piece of text into lines, but with line breaks inserted at just the right places to make sure that no line is longer than the column number. You try to break lines at word boundaries.
First test, should not wrap text that fits.
def test_should_not_wrap_text_that_fits(self):
    self.assertEqual(wrap("text that fits", 20), "text that fits")
and the faked result:
def wrap(text, column):
    return "text that fits"
The test passes. Next step is to remove any duplication between the test and the implementation. Returning the result that the test is expecting is duplication and that can be removed by returning the passed in text:
def wrap(text, column):
    return text
Test passes and there's no more duplication between the test and the implementation.

Doing this wouldn't make a whole lot of sense in the real world. Passing this test is so simple that it would be best to use Obvious Implementation, but I think it serves as a nice introduction to the technique.

Second Test, wrapping text that does not fit on a line.
def test_wrapping_text_that_does_not_fit_on_a_line(self):
    self.assertEqual(wrap("text that does not fit", 15), "text that does\n not fit")


Faking It

It is possible to pass both tests by branching and faking the new result.
def wrap(text, column):
    if len(text) > column:
        return "text that does\n not fit"
    return text
Running the tests should give two passing tests. Now to start removing the duplication. I'm going to do it as slowly as possible to show how the technique can work.

Side note, speeding up and slowing down

The steps in Fake It may seem painfully slow for some, but this is a good thing.  Fake it allows me to do go slowly or quickly, depending on my confidence. At it's slowest I'm making the simplest possible change and running the tests to ensure that I'm at green. I can speed up by taking a few of those steps together. Full speed, where I just make the change I want, is Obvious Implementation. That's the big advantage that this approach has over Obvious Implementation and Triangulation, both of which force me to write all the code at once.


Removing duplication

That returned text is made up of three separate parts, two lines and a line break. After each step here the tests are run to ensure they are still green:
def wrap(text, column):
    if len(text) > column:
        return return "text that will " + "\n" + "not fit"
    return text
That first part is really just as much as will fit in the first column.
def wrap(text, column):
    if len(text) > column:
        return text[:column] + "\n" + "not fit"
    return text
And the second part is the remainder.
def wrap(text, column):
    if len(text) > column:
        return text[:column] + "\n" + text[column:]
    return text
Most of the duplication between the test and the implementation has been removed, but there is still some. That is, it will only handle one line break. It will be easier to remove this duplication if the duplicate return statements are refactored into one.

The first step is the make the return statements the same.
def wrap(text, column):
    wrapped = ''
    if len(text) > column:
        wrapped = text[:column] + "\n"
        text = text[column:]
        return wrapped + text
    return wrapped + text
Once they're the same, the redundant return can be removed.
def wrap(text, column):
    wrapped = ''
    if len(text) > column:
        wrapped = text[:column] + "\n"
        text = text[column:]
    return wrapped + text
By doing this I've changed the structure of the code without changing the behaviour. The solution still needs to be generalised to handle any amount of lines. That's got me thinking that my current test is not good enough. I want to take the approach of changing that if to a while, but I'd like to have some safety in the move.

Triangulation can be useful

This is a situation where triangulation can be useful. I'm going to introduce one more test to ensure that all is good. The test will be wrapping text over multiple lines.
def wrapping_text_over_multiple_lines(self):
    self.assertEqual(wrap("wrapping text over multiple lines", 9), "wrapping \ntext over\n multiple\n lines")
I can pass all three tests now by changing the if to a while and appending to the wrapped text.
def wrap(text, column):
    wrapped = ''
    while len(text) > column:
        wrapped += text[:column] + "\n"
        text = text[column:]
    return wrapped + text


Not done yet

There's one final step I have to take if I want to tidy everything up. When I added that extra test at the end, I also added duplication between the tests because wrapping_text_over_multiple_lines tests the same behaviour as wrapping_text_that_does_not_fit_on_a_line. I can prove this by changing anything in the implementation to break wrapping_text_that_does_not_fit_on_a_line. This will in turn break wrapping_text_over_multiple_lines every time. The same is not true in reverse, so wrapping_text_over_multiple_lines is the test that I want to keep.

Disclaimer

This is not a perfect implementation of word wrap, the intention is not create create a perfect word wrap, just to show how fake it works.
Fake it works really well when you can get it to work, but the biggest difficulty I find is seeing the simplest change to make and sometimes even how or what to fake it. When I'm stuck I end up taking big steps or triangulating to help me move. Often, doing either of these steps will help me understand what I should have done, so I can quickly revert and go back to faking it.

No comments:

Post a Comment