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.
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.Crearemos un pequeño paquete llamado conversor. Su primer comportamiento será convertir grados Celsius a grados Fahrenheit.
Más adelante podremos agregar nuevas conversiones, pero empezaremos con un solo comportamiento observable.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Este proyecto mínimo nos da una base más limpia para los próximos temas:
pytest sabe dónde buscar las pruebas.conversor: verifica que ejecutaste python -m pip install -e . desde la carpeta donde está pyproject.toml.tests y se llame test_*.py.src/conversor y el archivo __init__.py.Agrega una nueva función llamada fahrenheit_a_celsius siguiendo el ciclo de TDD:
32 grados Fahrenheit a 0 grados Celsius.python -m pytest y verifica que falle.src/conversor/temperatura.py.212 a 100.Antes de continuar, verifica lo siguiente:
src y tests.pyproject.toml con configuración mínima.conversor está dentro de src.python -m pip install -e ..python -m pytest.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.