3. Preparación de un proyecto mínimo para practicar TDD

3.1 Objetivo del tema

En los temas anteriores usamos archivos simples para concentrarnos en el ciclo rojo, verde y refactor. En este tema prepararemos un proyecto mínimo un poco más ordenado, con una carpeta para el código de producción y otra carpeta para las pruebas.

Esta estructura nos permitirá practicar TDD de una forma más parecida a la que usaremos en proyectos reales, sin agregar todavía herramientas avanzadas ni configuraciones innecesarias.

Objetivo práctico: crear un proyecto Python pequeño con carpetas src y tests, configurarlo para pytest y comprobarlo con un primer ciclo de TDD.

3.2 Por qué ordenar el proyecto

Cuando todos los archivos están en una sola carpeta, los ejercicios iniciales son más fáciles de seguir. Pero a medida que el proyecto crece, esa organización se vuelve confusa.

Separar el código de producción y las pruebas nos ayuda a ver con claridad qué archivos forman parte del programa y cuáles verifican su comportamiento.

  • src: contiene el código de producción.
  • tests: contiene las pruebas automatizadas.
  • pyproject.toml: contiene configuración básica del proyecto.

3.3 Proyecto que construiremos

Crearemos un pequeño paquete llamado conversor. Su primer comportamiento será convertir grados Celsius a grados Fahrenheit.

Requisito inicial: convertir 0 grados Celsius en 32 grados Fahrenheit.

Más adelante podremos agregar nuevas conversiones, pero empezaremos con un solo comportamiento observable.

3.4 Crear la carpeta del proyecto

Como ya conocemos el entorno virtual, ahora solo creamos una carpeta nueva para el ejercicio.

mkdir conversor-tdd
cd conversor-tdd

Si el entorno virtual no está activo, actívalo antes de instalar o ejecutar herramientas.

3.5 Crear la estructura de carpetas

Dentro del proyecto creamos estas carpetas:

mkdir src
mkdir src\conversor
mkdir tests

En Linux o macOS puedes usar:

mkdir -p src/conversor tests

La carpeta src/conversor será el paquete Python. La carpeta tests guardará los archivos de prueba.

3.6 Estructura esperada

Por ahora la estructura del proyecto será:

conversor-tdd/
|-- src/
|   `-- conversor/
`-- tests/

Todavía no escribimos comportamiento. Solo preparamos el lugar donde vivirá el código.

3.7 Crear pyproject.toml

El archivo pyproject.toml permite describir el proyecto y configurar herramientas. Usaremos una configuración mínima.

Archivo a crear: pyproject.toml

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "conversor-tdd"
version = "0.1.0"
requires-python = ">=3.10"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
testpaths = ["tests"]

Con esta configuración indicamos que el código del paquete estará dentro de src y que pytest debe buscar pruebas en tests.

3.8 Instalar el proyecto en modo editable

Para que Python pueda importar el paquete conversor desde la carpeta src, instalamos el proyecto en modo editable:

python -m pip install -e .

El modo editable permite trabajar sobre los archivos fuente sin reinstalar el paquete después de cada cambio.

3.9 Crear el paquete Python

Un paquete Python necesita un archivo __init__.py. En este caso lo dejaremos vacío.

Archivo a crear: src/conversor/__init__.py

El archivo vacío alcanza para indicar que conversor es un paquete.

3.10 Rojo: escribir la primera prueba

Empezamos el desarrollo con una prueba. Todavía no existe el módulo temperatura.py ni la función celsius_a_fahrenheit.

Archivo a crear: tests/test_temperatura.py

from conversor.temperatura import celsius_a_fahrenheit


def test_convertir_cero_celsius_a_fahrenheit():
    resultado = celsius_a_fahrenheit(0)

    assert resultado == 32

Ejecutamos:

python -m pytest

La prueba debe fallar porque todavía no implementamos el módulo.

3.11 Leer el fallo

Un fallo posible es:

ModuleNotFoundError: No module named 'conversor.temperatura'

El mensaje nos indica el siguiente paso mínimo: crear el archivo temperatura.py dentro del paquete conversor.

3.12 Verde: crear el código mínimo

Creamos el módulo y escribimos la implementación mínima para pasar la prueba actual.

Archivo a crear: src/conversor/temperatura.py

def celsius_a_fahrenheit(celsius):
    return 32

Ejecutamos nuevamente:

python -m pytest

La prueba debería pasar. La función todavía no es general, pero cumple el único ejemplo que tenemos.

3.13 Agregar un segundo ejemplo

Ahora agregamos otra prueba para obligar a generalizar la fórmula.

Archivo a modificar: tests/test_temperatura.py

from conversor.temperatura import celsius_a_fahrenheit


def test_convertir_cero_celsius_a_fahrenheit():
    resultado = celsius_a_fahrenheit(0)

    assert resultado == 32


def test_convertir_cien_celsius_a_fahrenheit():
    resultado = celsius_a_fahrenheit(100)

    assert resultado == 212

Ejecutamos python -m pytest. La segunda prueba debe fallar porque la función devuelve siempre 32.

3.14 Verde: implementar la fórmula

La nueva prueba justifica implementar la fórmula real.

Archivo a modificar: src/conversor/temperatura.py

def celsius_a_fahrenheit(celsius):
    return celsius * 9 / 5 + 32

Ejecutamos:

python -m pytest

Si ambas pruebas pasan, el comportamiento está funcionando para los dos ejemplos.

3.15 Refactor: mejorar las pruebas

Las dos pruebas verifican el mismo comportamiento con distintos datos. Podemos expresarlo con parametrización.

Archivo a modificar: tests/test_temperatura.py

import pytest

from conversor.temperatura import celsius_a_fahrenheit


@pytest.mark.parametrize(
    "celsius, esperado",
    [
        (0, 32),
        (100, 212),
    ],
)
def test_convertir_celsius_a_fahrenheit(celsius, esperado):
    assert celsius_a_fahrenheit(celsius) == esperado

Ejecutamos python -m pytest. Si todo sigue en verde, refactorizamos la prueba sin cambiar el comportamiento esperado.

3.16 Estructura final del proyecto

Al finalizar, el proyecto queda así:

conversor-tdd/
|-- pyproject.toml
|-- src/
|   `-- conversor/
|       |-- __init__.py
|       `-- temperatura.py
`-- tests/
    `-- test_temperatura.py

Esta estructura es pequeña, pero ya separa responsabilidades y permite crecer con más orden.

3.17 Ventajas para practicar TDD

Este proyecto mínimo nos da una base más limpia para los próximos temas:

  • Las pruebas están separadas del código de producción.
  • El paquete puede importarse como lo haría otro código Python.
  • pytest sabe dónde buscar las pruebas.
  • El proyecto puede crecer sin mezclar archivos de distinta responsabilidad.
  • La estructura permite repetir el ciclo rojo, verde y refactor con más claridad.

3.18 Problemas frecuentes

  • No se puede importar conversor: verifica que ejecutaste python -m pip install -e . desde la carpeta donde está pyproject.toml.
  • pytest no encuentra pruebas: revisa que el archivo esté dentro de tests y se llame test_*.py.
  • El paquete no aparece: revisa que exista la carpeta src/conversor y el archivo __init__.py.
  • La prueba pasa sin fallar primero: quizá escribiste el código antes de la prueba. En TDD necesitamos observar primero la etapa roja.

3.19 Ejercicio propuesto

Agrega una nueva función llamada fahrenheit_a_celsius siguiendo el ciclo de TDD:

  • Escribe primero una prueba para convertir 32 grados Fahrenheit a 0 grados Celsius.
  • Ejecuta python -m pytest y verifica que falle.
  • Implementa el código mínimo en src/conversor/temperatura.py.
  • Agrega un segundo ejemplo para convertir 212 a 100.
  • Refactoriza si aparece duplicación o si la prueba puede quedar más clara.

3.20 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • El proyecto tiene carpetas src y tests.
  • Existe el archivo pyproject.toml con configuración mínima.
  • El paquete conversor está dentro de src.
  • Ejecutaste python -m pip install -e ..
  • La prueba inicial falló antes de implementar el código.
  • La suite pasa con python -m pytest.
  • Comprendes por qué esta estructura es más ordenada que tener todos los archivos juntos.

3.21 Conclusión

En este tema preparamos un proyecto mínimo para practicar TDD con una estructura más realista. Creamos carpetas separadas para código y pruebas, configuramos el proyecto con pyproject.toml y comprobamos la estructura mediante un primer ciclo rojo, verde y refactor.

En el próximo tema escribiremos una primera prueba fallida a partir de un requisito, poniendo especial atención en cómo convertir una necesidad del usuario en una especificación ejecutable.