3. TDD

TDD (Test Driven Development) is the testing practice most used by developers. It consists of a flow opposite to traditional testing: you write the test first and only add new code when that test fails. It sounds strange at first. Let's see the cycle with the creation of a sword.

  1. You create the sheath where it must fit.
  2. You forge a sword and try to insert it. The first time it won't go in.
  3. You give the minimal blows to bring it closer to the right shape.
  4. You try again. If it still doesn't go in, you keep working.
  5. Now it goes in smoothly.
  6. You refactor: you remove impurities, marks from the work, and give it shine.

TDD steps

Flow

More than a framework, TDD is a methodology. It has a very strict workflow that gives more priority to the test than to the code.

TDD flow

  1. You create a test.
  2. You run all the previous tests and the new one. The first time it will fail.
  3. You write the simplest code that makes it pass.
  4. You run all the tests. If it fails, you go back to the previous step.
  5. You refactor and run the tests on every change: you move the code to its place, remove repetitions, document, split big functions into small ones. If something fails, you go back.
  6. You create the test for the next feature and start the cycle again.

Example

We're going to write a function that tells us the state of water (solid, liquid or gas) according to the temperature.

1. You start with the skeleton

I create a file water.py with the minimal code:

def get_state(temperature):
    """Return 'solid', 'liquid' or 'gas' for a given temperature in Celsius."""
    return ""

2. You create the test

In test_water.py I check that it returns solid between -273 degrees (the minimum) and 0:

import pytest

from water import get_state


@pytest.mark.parametrize("temperature", range(-273, 1))
def test_solid(temperature):
    assert get_state(temperature) == "solid"

3. You run the test

pytest

4. Does it fail? On to the next point.

It fails, of course. There's no logic yet.

5. You write the minimal code that makes it pass

def get_state(temperature):
    if temperature <= 0:
        return "solid"

I run it again. It passes! Now the next test, liquid?

@pytest.mark.parametrize("temperature", range(1, 100))
def test_liquid(temperature):
    assert get_state(temperature) == "liquid"

I run... and it fails, logically. I refactor:

def get_state(temperature):
    if temperature <= 0:
        return "solid"
    if 0 < temperature < 100:
        return "liquid"

It passes. And so the wheel keeps turning until all the cases are covered.

6. Can't write any more tests? You're done

My function is complete and tested:

def get_state(temperature):
    if temperature <= 0:
        return "solid"
    if 0 < temperature < 100:
        return "liquid"
    return "gas"

Triangulation: break the fear of the blank page

What if I don't know where to start writing the test? Triangulation gets you around the initial block. Careful, don't confuse it with classic TDD.

Instead of first writing the test that fails, you write three tests that pass, each with a slightly more general case, and you refactor to eliminate the duplication between them.

Let's suppose we want a function that detects whether a word is an anagram of another (same letters, different order).

First test, a very simple case:

def test_anagram_simple():
    assert is_anagram("amor", "roma") is True


def is_anagram(s1, s2):
    return sorted(s1) == sorted(s2)

Second test, now with spaces:

def test_anagram_with_spaces():
    assert is_anagram("amor", "a rom") is True


def is_anagram(s1, s2):
    return sorted(s1.replace(" ", "")) == sorted(s2.replace(" ", ""))

Third test, with uppercase:

def test_anagram_with_uppercase():
    assert is_anagram("Amor", "ROMA") is True


def is_anagram(s1, s2):
    return sorted(s1.replace(" ", "").lower()) == sorted(s2.replace(" ", "").lower())

Each step generalizes a little more. In the end you have a function that solves the complete case, and a handful of tests that you can unify with parametrization.

Supporting material

TDD flow diagram

Activity 1

Build your own password validator. Depending on which requirements it meets, it will return its security level, between 0 and 5. The function will be password_strength.

  • More than 10 characters (level +1).
  • More than 20 characters (level +1).
  • Alphanumeric (level +1).
  • Contains special characters such as /, _, $... (level +1).
  • One or more spaces (level +1).

Use the TDD methodology.

Activity 2

Create a function that greets.

greet("Conan")
# Will return -> 'Hello Conan'

Use the TDD methodology.

Activity 3

Create a function that returns a list with the even numbers from 0 up to the argument you indicate.

even_numbers(6)
# Will return -> [0, 2, 4, 6]

Use the TDD methodology and take advantage of triangulation to get started.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Desafíos de programación atemporales y multiparadigmáticos

Desafíos de programación atemporales y multiparadigmáticos

Te encuentras ante un librillo de actividades, divididas en 2 niveles de dificultad. Te enfrentarás a los casos más comunes que te puedes encontrar en pruebas técnicas o aprender conceptos elementales de programación.

Buy the book

Will you buy me a coffee?

This is how I keep writing without ads or paywalls.

Comments

There are no comments yet.