Test-driven development [TDD] is a way of thinking (Part 1)

Julio
Julio
May 15 · 6 min read

At the IBM Garage, software products are built using an agile methodology called test-driven development — or TDD, for short. As a developer there, I have found many explanations of TDD that are not nailed well enough.

Sometimes we say we test drive to show that the code works as the business expects. But we can write code, and then afterwards, write the tests to show that the code meets requirements, nevertheless.

Studies show that test driving improves code quality. [1][2] But why does test driving improve code quality?

When we first learn TDD, we learn a big bag of terminologies and complicated ideas: write tests first, the red -> green -> refactor cycle, the Transformation Priority Premise, behavior-driven development [BDD], and fill-in-the-blank-driven development. [3] However, buried under all that detail lies one fundamental idea:

The two ways of thinking are forward and backwards.

Say we’re getting a pet for Christmas, but we’re low maintenance and wondering if this pet needs to be bathed on a regular basis. We’re given two clues about the pet: it loves yarn, and it sleeps most of the day.

Based on our prior knowledge about pet animals, we surmise the following subset of inferences:

(1.1) If it loves yarn and sleeps most of the day, then it is a cat.(1.2) If it is loyal and a best friend, then it is a dog.(1.3) If it is a cat, then it licks itself clean.(1.4) If it is a dog, then it needs shampoo.

Does the pet lick itself clean?

It’s obvious that the answer is yes, and we know that the pet is a cat. The question is: how did we come up with the answer?

We might have substituted the facts with the given implication (1.1). We know the pet in the box loves yarn and sleeps most of the day, so we conclude that the pet is a cat. Given that conclusion, we then used implication (1.3) to conclude that the cat licks itself clean.

Another way we could have answered the question is to start with the question itself, and not the facts.

What licks itself clean? (1.3) tells us that a cat does. We can then take that conclusion and show the converse of (1.1) holds true.

Both approaches achieve the same outcome, but the former works forward from (1.1) to (1.3) by starting with existing data and facts to find an answer, while the latter works backwards from (1.3) to (1.1) by starting with a list of questions or hypotheses to then find supportive data and facts.

The Fahrenheit to Celsius problem…

…is the first coding assignment at the IBM Garage when learning TDD. It’s very easy. Students always solve it, just like how we easily solved the pet for Christmas problem.

But what we care about is how the problem is solved.

The units of measure of temperatures are given in Fahrenheit [F] and Celsius [C] degrees. The test cases that must pass are as follows:

(2.1) 32 F degrees must be 0 C degrees.(2.2) 212 F degrees must be 100 C degrees.(2.3) 50 F degrees must be 10 C degrees.

To pass the first test (2.1), we can just return 0.

const fahrenheitToCelsius = (f) => {    return 0;}

Easy.

Now, we’ll try passing the second test (2.2) using the Transformation Priority Premise. The premise provides transformation rules that allow our code to be more generic as the tests become more specific. These rules prevent the code from mirroring the tests. [4]

Such code might look like this:

const fahrenheitToCelsius = (f) => {    if (f === 32) return 0;    if (f === 212) return 100;    // … etc.}

Instead, the second test (2.2) will force us to solve the formula: (f — 32) * 5/9. Question: how did we come up with that?

Chances are you googled the answer, or you already knew the Fahrenheit to Celsius conversion. If we do this, then we are not test-driving our code.

To show what I mean, suppose there is a similar exercise for which there exists an unknown unit of temperature measure [X]. Let’s try to solve the equation to convert Fahrenheit to the unknown measure.

The test cases that must pass are as follows:

(3.1) 18 F degrees must be 0 X degrees.(3.2) 212 F degrees must be 4 X degrees.(3.3) 115 F degrees must be 2 X degrees.

Just like before in the Fahrenheit to Celsius problem, to pass the first test (3.1), we can just return 0.

const fahrenheitToUnknown = (f) => {    return 0;}

Just as easy.

Using the Transformation Priority Premise, the second test (3.2) will force us to solve the formula. But what is the formula?

Unlike the Fahrenheit to Celsius problem, we’re a bit more stuck on solving the Fahrenheit to the unknown measure problem. That is because our tests never drove our code.

TDD is the practice of thinking backwards.

When we think backwards, we drive with the business requirements. Instead, we were thinking forward — we drove using facts, data, prior knowledge or whatever we might call it.

In a programming context, the code is the data and tests are the business requirements. Thinking forward, we substitute the data (f — 32) * 5/9 and check if it satisfies the requirements.

Thinking backwards, we take the business requirements — the tests — and search for the data that satisfies those requirements. How we search matters, which is why we use the Transformation Priority Premise — an ordered list of inference rules to help us work backwards — just like how we had our four rules in the pet problem.

Solving the problem by thinking backwards…

…leads us to the second rule of the Transformation Priority Premise (constant -> constant+) to pass the second test (2.2). This transformation means we convert a constant, our 0 that is currently returned by the Fahrenheit to Celsius function, into a more complex constant.

This rule can be broken down even further because there are many ways to create a complex constant where one is more complex than the other. Addition (+,-) -> multiplication (x,/,%) -> exponentiation (^,log) -> trig (sin, cos) -> …

const fahrenheitToCelsius = (f) => {    return 0;}

Our current formula is 0. Of course, there is no significance about zero here — the formula just mirrors the test to pass the first test case.

So, if we apply the simplest transformation (constant -> constant+addition), then, to keep the first test passing, we would transform 0 to f — 32. In the context of the function, the argument does not change in value, so that, by definition, we return a complex constant.

const fahrenheitToCelsius = (f) => {    return f - 32;}

Already, I get the sense we’re closer to the formula we need. Let’s do this!

Let’s experiment with the next (constant -> constant+) transformation: multiplication. This transformation actually makes a lot of sense because any number multiplied by zero is zero, so our first test case (0.1) will remain passing.

We just need to find the right factor to multiply c = (f — 32) * x. Solving the factor algebraically is simple if we substitute our expected Fahrenheit to Celsius constants — 212 and 100, respectively:

x(f-32) = c      x = c/(f-32)        = 100/(212-32)        = 100/180        = 5/9

Therefore,

const fahrenheitToCelsius = (f) => {    return (f - 32) * 5/9;}

Simple, right?

Working backwards led us to solve the problem without prior knowledge of the formula, and the same can be done for the unknown unit of temperature measure, [X].

One type of thinking may be better than the other depending on the circumstance.

If we know what decision has to be made, backwards thinking will help us arrive at the answer a lot more quickly. This is why TDD makes sense when a developer has to satisfy a story requirement.

If we are not sure what decision has to be made or there are too many decisions that can be made, forward thinking is a better option. In part 2, we will cover when you should think forward or backwards.

> Learn more about how you can co-create with the IBM Garage.

Special thanks to Jeanette Pranin and Davia Denisco for reading early drafts.

References:

  1. Borle N, Feghhi M, Stroulia E, Greiner R, Hindle A (2017) Analyzing The Effects of Test Driven Development In GitHub, http://softwareprocess.es/pubs/borle2017EMSE-TDD.pdf.
  2. Nagappan N, Maximilien M, Bhat T, Williams L (2008) Realizing quality improvement through test driven development: results and experiences of four industrial teams, https://www.microsoft.com/en-us/research/wp-content/uploads/2009/10/Realizing-Quality-Improvement-Through-Test-Driven-Development-Results-and-Experiences-of-Four-Industrial-Teams-nagappan_tdd.pdf.
  3. Acosta J, Gajda K, Test-driven development, https://www.ibm.com/cloud/garage/practices/code/practice_test_driven_development/.
  4. “…code that [mirrors] the tests…” in, Robert C. Martin (2013) The Transformation Priority Premise, https://blog.cleancoder.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html.

IBM Garage

Startup speed at enterprise scale.

Julio

Written by

Julio

Security. Bitcoin. Agile practitioner. Software engineer at the Cloud Garage | NYC.

IBM Garage

Startup speed at enterprise scale.