Cuando un proyecto crece, mezclar código principal y pruebas en una sola carpeta vuelve más difícil entender qué archivos forman parte de la aplicación y cuáles sirven para verificarla.
En este tema organizaremos un proyecto con carpetas separadas para código fuente y pruebas. También veremos cómo importar módulos desde las pruebas y cómo ejecutar la suite desde la raíz del proyecto.
El proyecto quedará organizado así:
organizacion-demo/
|-- src/
| `-- tienda/
| |-- __init__.py
| |-- descuentos.py
| `-- productos.py
`-- tests/
|-- __init__.py
|-- test_descuentos.py
`-- test_productos.py
La carpeta src contiene el código de la aplicación. La carpeta tests contiene las pruebas automatizadas.
Crea un proyecto nuevo:
mkdir organizacion-demo
cd organizacion-demo
Ejecutaremos todos los comandos desde esta carpeta principal.
Crea la carpeta de código fuente y la carpeta de pruebas:
mkdir src
mkdir tests
Luego crea la carpeta del paquete principal:
mkdir src\tienda
En Linux o macOS, el comando equivalente es:
mkdir -p src/tienda
Para que Python trate estas carpetas como paquetes importables, crea archivos __init__.py:
New-Item src\tienda\__init__.py -ItemType File
New-Item tests\__init__.py -ItemType File
En Linux o macOS:
touch src/tienda/__init__.py
touch tests/__init__.py
El archivo puede estar vacío. Su presencia ayuda a que las importaciones sean más predecibles, especialmente con unittest.
Dentro de src\tienda, crea descuentos.py:
def aplicar_descuento(precio, porcentaje):
if precio < 0:
raise ValueError("El precio no puede ser negativo")
if porcentaje < 0 or porcentaje > 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
return precio - (precio * porcentaje / 100)
def tiene_descuento(porcentaje):
return porcentaje > 0
Este módulo contiene funciones relacionadas con precios y descuentos.
Ahora crea productos.py dentro de src\tienda:
def normalizar_nombre(nombre):
return nombre.strip().title()
def crear_producto(nombre, precio):
if not nombre or not nombre.strip():
raise ValueError("El nombre es obligatorio")
if precio <= 0:
raise ValueError("El precio debe ser mayor que cero")
return {
"nombre": normalizar_nombre(nombre),
"precio": precio,
}
Separar módulos por responsabilidad ayuda a ubicar el código y a escribir pruebas más específicas.
Dentro de tests, crea test_descuentos.py:
import unittest
from src.tienda.descuentos import aplicar_descuento, tiene_descuento
class TestDescuentos(unittest.TestCase):
def test_aplicar_descuento(self):
resultado = aplicar_descuento(1000, 10)
self.assertEqual(resultado, 900)
def test_porcentaje_cero_no_tiene_descuento(self):
resultado = tiene_descuento(0)
self.assertFalse(resultado)
def test_porcentaje_mayor_a_cero_tiene_descuento(self):
resultado = tiene_descuento(15)
self.assertTrue(resultado)
def test_porcentaje_invalido_lanza_error(self):
with self.assertRaises(ValueError):
aplicar_descuento(1000, 120)
La importación usa src.tienda.descuentos porque el paquete tienda está dentro de la carpeta src.
Dentro de tests, crea test_productos.py:
import unittest
from src.tienda.productos import crear_producto, normalizar_nombre
class TestProductos(unittest.TestCase):
def test_normalizar_nombre(self):
resultado = normalizar_nombre(" mouse gamer ")
self.assertEqual(resultado, "Mouse Gamer")
def test_crear_producto_valido(self):
resultado = crear_producto("teclado", 50000)
self.assertEqual(resultado, {
"nombre": "Teclado",
"precio": 50000,
})
def test_nombre_vacio_lanza_error(self):
with self.assertRaises(ValueError):
crear_producto(" ", 50000)
def test_precio_cero_lanza_error(self):
with self.assertRaises(ValueError):
crear_producto("mouse", 0)
Cada archivo de prueba se enfoca en un módulo del código fuente.
Desde la raíz del proyecto, ejecuta:
python -m unittest
La salida esperada será similar a:
........
----------------------------------------------------------------------
Ran 8 tests in 0.001s
OK
Es importante ejecutar el comando desde organizacion-demo, no desde tests ni desde src.
Para ver el nombre de cada prueba:
python -m unittest -v
Esto ayuda a confirmar qué archivos y métodos fueron descubiertos por unittest.
También puedes pedir descubrimiento explícito en la carpeta tests:
python -m unittest discover -s tests
La opción -s indica desde qué carpeta comienza la búsqueda de pruebas.
Para ejecutar solo las pruebas de productos:
python -m unittest tests.test_productos
Observa que usamos puntos en lugar de barras y no escribimos la extensión .py.
Separar carpetas tiene varias ventajas prácticas:
pytest, cobertura o integración continua más adelante.| Estructura | Ejemplo | Cuándo usarla |
|---|---|---|
| Simple | modulo.py y test_modulo.py en la misma carpeta |
Ejercicios muy pequeños o pruebas rápidas. |
| Separada | app/ y tests/ |
Proyectos chicos y medianos. |
| Con src | src/paquete/ y tests/ |
Proyectos que quieren una separación más estricta. |
Las pruebas deben importar el código igual que lo haría otro módulo del proyecto. En este ejemplo usamos:
from src.tienda.productos import crear_producto
Si ejecutas las pruebas desde otra carpeta, Python puede no encontrar src. Por eso repetimos esta regla: ejecuta las pruebas desde la raíz del proyecto.
No conviene usar nombres de archivos que choquen con módulos estándar o paquetes conocidos. Por ejemplo, evita nombres como:
unittest.py
pytest.py
email.py
json.py
Si creas un archivo con uno de esos nombres, Python puede importar tu archivo en lugar del módulo esperado y producir errores difíciles de entender.
Al terminar, la carpeta debería quedar así:
organizacion-demo/
|-- src/
| `-- tienda/
| |-- __init__.py
| |-- descuentos.py
| `-- productos.py
`-- tests/
|-- __init__.py
|-- test_descuentos.py
`-- test_productos.py
__init__.py: algunas importaciones por paquete pueden fallar.mkdir organizacion-demo
cd organizacion-demo
mkdir src
mkdir tests
mkdir src\tienda
New-Item src\tienda\__init__.py -ItemType File
New-Item tests\__init__.py -ItemType File
python -m unittest
python -m unittest -v
python -m unittest discover -s tests
python -m unittest tests.test_productos
src puede contener el paquete principal de la aplicación.tests debe contener archivos de prueba con nombres claros.En este tema organizamos un proyecto separando código fuente y pruebas. Creamos un paquete dentro de src, agregamos pruebas dentro de tests y ejecutamos la suite desde la raíz del proyecto.
En el próximo tema veremos convenciones de nombres para archivos, clases y métodos de prueba, un detalle importante para que las herramientas puedan descubrir y ejecutar la suite correctamente.