The benefits of test-driven-development

Erin
My Own Utopia
Published in
5 min readAug 13, 2019
René Magritte, The Treachery of Images , 1929, Los Angeles County Museum of Art

John Berger’s Ways of Seeing begins “Seeing comes before words. The child looks and recognises before it can speak.”

In learning how to code, what I’ve enjoyed and found the most helpful was diagramming the space of the problem at hand. As a visual learner, I haven’t found many resources that use visual explanations (in diagrammatic-form) to describe what the code is doing, and as such, at times it can feel like a multi-step process to understanding the problem, translating this to my understanding of what is being asked, creating a diagram or a mental model of what the relationships are between the problem and what I think the solution should be, to finally writing the code.

Berger continues, “It is seeing which establishes our place in the surrounding world; we explain that world with words, but words can never undo the fact that we are surrounded by it.”

Taking this step to see the problem, and writing tests alongside the code at first feels unwieldy, and piles up the amount of work that needs to be done to get to the solution. But taking this step has incredible value in the long-run, and creates confidence that the final solution delivered is a thoughtful and empathetic interpretation about the context in which the solution will be used. We can try to code our way out of the problem, but we can’t guarantee that our code will remove the error, unless we keep the context of the problem in mind.

For me, once I had drawn it, I had something concrete to work from. I could see what I thought the solution needed to be. So why test it?

I’ve drawn it, so it must be bullet-proof, right?

Within test-driven-development, we’re given the foresight to define what our problems are, and determine if our code is behaving according to what we said our outcome needs to be. We can test to make sure that what we think we’re solving is actually what what we’re trying to solve.

The prime example — Magritte’s 1929 painting “The Treachery of Images”, shows a pipe, with the words “Ceci n’est pas une pipe” (“This is not a pipe”) scrawled below. How could this be?

Testing our theories, we can verify — of course it’s not a pipe. It’s a painting. Sometimes you can get caught up in thinking you’ve identified exactly what you’re looking for, and test-driven development verifies our theories. We approach each test case that covers just enough, each step of the way.

The approach was defined by the American software engineer Kent Beck, who had also co-authored the planning approach of class-responsibility-collaboration cards. This approach allows developers to see the interactions between the different ‘players’ in our program, and helps to keep solutions simple by not getting too weighed down by unnecessary details and components. The distinction between the responsibilities of the different areas of our program is made clear.

Test-driven-development keeps our questions alongside the software development. Test cases are written according to the requirements we need, and code that does not meet the requirements are not added to the final product. Duplicates are removed, and the code should explain exactly what it’s trying to do.

There are just three ‘laws’ that the American software engineer Robert Cecil Martin, or “Uncle Bob” has provided us to begin our journey:

1. You are not allowed to write any production code unless it is to make a failing unit test pass.

2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.

3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

It is not too dissimilar from what Dieter Rams, the German industrial designer, expressed in his Ten Principles for Good Design. The way we design and build our software must be useful, and satisfy the actual need. After all, why else would we include it?

Dieter also explains that good design is thorough down to the last detail. Considering the consequences of how our product is built shows ‘respect for the user’. I’m sure we don’t need to think very hard for an example of a piece of technology that we have interacted with, that may be packed-full with features but doesn’t successfully do the one thing we needed for it to do.

However, my favourite principle is the final — ‘Good design is as little design as possible.’ Focus on getting it right, and making it the best, and unencumbered by a veneer of attributes that aren’t properly put through the assessment of the tests written during this phase of production.

This approach also relies on the process of ‘refactoring’, or to borrow terminology from the writing process, editing our code, invites us to approach our code again, and examine if it says simply what we wish for it to do. Sometimes to get to the solution we have to get there via the longer road. But once we know what we’re trying to say, we can see what is unnecessary, and take it out.

‘Abstraction’ also begins to come into play at this stage. Have we tried to say too much with this single method? Should we spell it out just once and then simply reuse what has already been explained? As coders we are writers, but we also have the freedom of allowing the computer to continue to speak for us.

In our final development, we should be left with code that shows us cleanly what it’s meant to do. We have fully tested it and know that it serves the purpose we have considered. And most importantly, in testing, we will have ensured that our code won’t continue to return a painting of a pipe, when what we really wanted was the real thing.

--

--