9. Organización de carpetas: código fuente y pruebas

9.1 Objetivo del tema

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.

Idea clave: una estructura clara evita errores de importación y facilita mantener las pruebas a medida que el proyecto crece.

9.2 Estructura que construiremos

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.

9.3 Crear la carpeta del proyecto

Crea un proyecto nuevo:

mkdir organizacion-demo
cd organizacion-demo

Ejecutaremos todos los comandos desde esta carpeta principal.

9.4 Crear las carpetas principales

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

9.5 Crear archivos __init__.py

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.

9.6 Crear el módulo descuentos.py

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.

9.7 Crear el módulo productos.py

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.

9.8 Crear pruebas para descuentos

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.

9.9 Crear pruebas para productos

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.

9.10 Ejecutar todas las pruebas

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.

9.11 Ejecutar con salida detallada

Para ver el nombre de cada prueba:

python -m unittest -v

Esto ayuda a confirmar qué archivos y métodos fueron descubiertos por unittest.

9.12 Ejecutar una carpeta específica

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.

9.13 Ejecutar un archivo 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.

9.14 Por qué separar src y tests

Separar carpetas tiene varias ventajas prácticas:

  • El código principal queda separado del código de prueba.
  • Es más fácil encontrar qué prueba corresponde a cada módulo.
  • La estructura se parece más a la de proyectos reales.
  • Evita que archivos de prueba se mezclen con módulos de producción.
  • Facilita agregar herramientas como pytest, cobertura o integración continua más adelante.

9.15 Comparar estructuras posibles

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.

9.16 Importaciones en las pruebas

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.

9.17 Evitar nombres conflictivos

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.

9.18 Estructura final del proyecto

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

9.19 Errores frecuentes

  • Ejecutar pruebas desde la carpeta incorrecta: puede provocar errores de importación.
  • Olvidar __init__.py: algunas importaciones por paquete pueden fallar.
  • Mezclar código y pruebas sin criterio: vuelve más difícil mantener el proyecto.
  • Nombrar archivos como módulos estándar: puede romper importaciones.
  • Importar con rutas de archivo: en Python se importan módulos con puntos, no con barras.

9.20 Comandos usados en este tema

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

9.21 Qué debes recordar de este tema

  • Separar código fuente y pruebas mejora la claridad del proyecto.
  • src puede contener el paquete principal de la aplicación.
  • tests debe contener archivos de prueba con nombres claros.
  • Ejecutar desde la raíz evita muchos errores de importación.
  • Las importaciones usan puntos, no rutas de archivo.
  • La estructura debe ayudar a leer, ejecutar y mantener la suite.

9.22 Conclusión

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.