Saltar a contenido

1.4.-Pruebas con pytest

1.4. Realización de pruebas con pytest

Idea principal

Programar no consiste únicamente en escribir código: también hay que comprobar que hace lo que se espera. En este apartado aprenderás a organizar un proyecto Python, a separar las funciones del código de prueba y a automatizar comprobaciones con pytest.

En los apartados anteriores has escrito programas pequeños, has utilizado variables, tipos y operadores, y has empezado a organizar el código mediante funciones. El siguiente paso es probar cada parte mientras la desarrollas. Así puedes detectar pronto un error, corregirlo y volver a comprobarlo sin tener que ejecutar manualmente todo el programa.

Este contenido se relaciona con los siguientes elementos de la normativa del módulo de Programación:

Código Descripción
RA 1 Reconoce la estructura de un programa informático, identificando y relacionando los elementos propios del lenguaje de programación utilizado.
CE b Se han creado proyectos de desarrollo de aplicaciones.
CE c Se han utilizado entornos integrados de desarrollo.

Qué deberías saber al terminar

Al acabar este apartado deberías poder:

  • explicar qué es una prueba automatizada y por qué resulta útil;
  • crear y activar un entorno virtual para un proyecto Python;
  • organizar el código fuente y las pruebas en carpetas separadas;
  • definir funciones pequeñas, con entradas y resultados claros;
  • escribir pruebas con assert y ejecutarlas con pytest;
  • probar varios casos con pytest.mark.parametrize;
  • interpretar el resultado de una ejecución de pruebas y corregir el código.

Mapa del tema

Seguiremos un ciclo sencillo de trabajo:

  1. preparar un entorno aislado;
  2. escribir una función con un comportamiento esperado;
  3. crear pruebas para casos normales, límite y erróneos;
  4. ejecutar las pruebas, interpretar el resultado y mejorar el código.

1. Probar mientras programas

Una prueba compara el comportamiento real de un programa con el comportamiento esperado. Por ejemplo, si una función suma dos números, podemos establecer que sumar(2, 3) debe devolver 5.

Las pruebas automatizadas son pequeños programas que realizan esa comprobación por nosotros. Se pueden ejecutar muchas veces y dejan un resultado claro:

  • correcta (passed): el resultado coincide con lo esperado;
  • fallida (failed): el programa responde de otra forma;
  • error (error): la prueba o el programa no han podido ejecutarse, por ejemplo, por un error de sintaxis o una importación incorrecta.

Una prueba no demuestra que no existan errores

Una prueba solo comprueba los casos que se han definido. Si los casos están bien elegidos, aumentan mucho la confianza en el programa, pero no sustituyen el análisis, la revisión del código ni las pruebas de integración.

2. Funciones como unidades que se pueden probar

Una función agrupa instrucciones bajo un nombre. Recibe datos mediante sus parámetros y puede devolver un resultado con return.

def calcular_importe(precio, unidades):
    """Devuelve el importe de una línea de pedido."""
    return precio * unidades

La llamada calcular_importe(12.5, 2) produce 25.0. Esta separación es importante para probar: la función recibe unos datos concretos y podemos comparar su resultado con el que esperamos, sin tener que introducir datos por teclado ni comprobar toda la salida de la aplicación.

2.1. Diseñar funciones fáciles de probar

En esta unidad no necesitamos aplicar todavía todos los principios de diseño, pero conviene adoptar estas pautas:

  • una función debe tener una responsabilidad clara;
  • debe recibir la información que necesita mediante parámetros;
  • debe devolver un resultado en lugar de mezclar siempre cálculo y print();
  • debe tener nombres expresivos y un docstring breve;
  • debe evitar depender de variables globales o de entradas que no podemos controlar durante una prueba.

Por ejemplo, es más fácil probar esta función:

def es_par(numero):
    """Indica si un número entero es par."""
    return numero % 2 == 0

que una función que pide el número y muestra el resultado a la vez:

def preguntar_si_es_par():
    numero = int(input("Introduce un número: "))
    print(numero % 2 == 0)

La segunda función puede ser útil en la interfaz de una aplicación, pero mezcla entrada, procesamiento y salida. La primera contiene la lógica y se puede probar directamente. Después, otra función puede encargarse de hablar con la persona usuaria.

Regla práctica

Si puedes llamar a una función con valores conocidos y comprobar su retorno sin usar el teclado, la pantalla, la red o un fichero, normalmente será más sencilla de probar.

3. Preparar el proyecto y su entorno virtual

Python permite instalar paquetes de terceros desde el índice PyPI (Python Package Index). Para evitar que las dependencias de un proyecto interfieran con las de otros, se utiliza un entorno virtual: una carpeta aislada en la que se instalan los paquetes del proyecto.

3.1. Comprobar Python y crear el entorno

En Linux y macOS, comprueba la versión instalada con:

$ python3 --version

En Windows puede ser necesario utilizar python o py. Desde la carpeta raíz del proyecto, crea el entorno usando el módulo incluido en Python:

$ python3 -m venv .venv

La opción venv es suficiente para este ejemplo; no es necesario instalar el paquete externo virtualenv. Activa el entorno con el comando correspondiente a tu sistema:

# Linux y macOS
$ source .venv/bin/activate

# Windows PowerShell
PS> .venv\Scripts\Activate.ps1

Cuando está activo, suele aparecer (.venv) al principio de la línea de órdenes. A partir de ese momento, pip instala los paquetes en este proyecto y no de forma global.

(.venv) $ python -m pip install --upgrade pip
(.venv) $ python -m pip install pytest

Usar python -m pip ayuda a asegurarse de que pip pertenece al mismo Python que ejecutará el proyecto. Para salir del entorno virtual:

(.venv) $ deactivate

No se debe subir .venv al control de versiones. Añade estas entradas a .gitignore:

.venv/
__pycache__/
*.pyc
.vscode/
.idea/
.DS_Store

3.2. Guardar las dependencias

Puedes consultar los paquetes instalados con pip list y guardar sus versiones en requirements.txt:

(.venv) $ python -m pip list
(.venv) $ python -m pip freeze > requirements.txt

Otra persona podrá recrear el entorno con:

$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python -m pip install -r requirements.txt

4. Organizar el código y las pruebas

Una organización sencilla separa el código que se entrega de los archivos que lo comprueban:

proyecto/
├── .venv/                  # Entorno virtual; no se sube al repositorio
├── src/                    # Código fuente
│   ├── __init__.py
│   └── main.py
├── tests/                  # Pruebas automatizadas
│   ├── __init__.py
│   └── test_main.py
├── .gitignore
├── requirements.txt
└── README.md

El archivo __init__.py permite tratar src como un paquete en esta estructura. Puede estar vacío; también puedes incluirlo en tests/ para mantener ambos directorios como paquetes. En Linux y macOS, el entorno contiene ejecutables en .venv/bin y paquetes instalados en .venv/lib (en Windows se organizan de otra forma). Los nombres de las pruebas deben empezar normalmente por test_ o terminar en _test.py para que pytest las encuentre automáticamente.

En src/main.py guardaremos la función que queremos probar:

def sumar(a, b):
    """Devuelve la suma de dos números."""
    return a + b

5. Escribir la primera prueba

En tests/test_main.py importamos la función y usamos una aserción (assert) para expresar el resultado esperado:

from src.main import sumar


def test_sumar_dos_numeros():
    # Preparar y ejecutar
    resultado = sumar(2, 3)

    # Comprobar
    assert resultado == 5

Esta prueba sigue el patrón Preparar, ejecutar y comprobar (Arrange, Act, Assert):

  1. se preparan los datos de entrada;
  2. se llama a la función;
  3. se compara el resultado con el valor esperado.

Una aserción no imprime un mensaje para que lo leamos. Si la condición es cierta, la prueba continúa; si es falsa, pytest informa del valor obtenido y del valor esperado.

Desde la raíz del proyecto, ejecuta todas las pruebas:

(.venv) $ python -m pytest

También puedes indicar expresamente la carpeta:

(.venv) $ python -m pytest tests

Una salida resumida correcta puede ser:

============================= test session starts =============================
collected 1 item

tests/test_main.py .                                                       [100%]

============================== 1 passed in 0.02s ===============================

El punto (.) representa una prueba superada. Si cambiamos temporalmente la aserción a assert resultado == 6, pytest la marcará como fallida y mostrará la diferencia. Lee ese informe antes de modificar código al azar: suele indicar el archivo, la línea, la expresión comprobada y los valores obtenidos.

6. Probar varios casos con parametrize

Una función no se valida con un único ejemplo. Conviene probar valores normales, casos límite y, cuando proceda, valores negativos o vacíos. El decorador pytest.mark.parametrize permite ejecutar la misma prueba con varios conjuntos de datos:

import pytest

from src.main import sumar


@pytest.mark.parametrize(
    "primer_sumando, segundo_sumando, esperado",
    [
        (0, 0, 0),
        (-1, 1, 0),
        (5, 5, 10),
        (2.5, 1.5, 4.0),
    ],
)
def test_sumar_varios_casos(primer_sumando, segundo_sumando, esperado):
    assert sumar(primer_sumando, segundo_sumando) == esperado

La lista contiene casos independientes. Si uno falla, pytest identifica también los parámetros utilizados, lo que facilita localizar el problema.

Elige casos con intención

No se trata de añadir valores al azar. Pregúntate qué puede ocurrir en el uso real: el caso habitual, el mínimo, el máximo, el cero, un valor negativo, una colección vacía o un dato incorrecto, según la función.

7. Probar errores esperados

Una función puede rechazar entradas que no tienen sentido. Por ejemplo, una división no puede utilizar cero como divisor. Si la función documenta que lanza ValueError, pytest permite comprobarlo con pytest.raises:

import pytest


def dividir(dividendo, divisor):
    """Divide dos números y rechaza el divisor cero."""
    if divisor == 0:
        raise ValueError("El divisor no puede ser cero")
    return dividendo / divisor


def test_dividir_rechaza_cero():
    with pytest.raises(ValueError, match="divisor"):
        dividir(10, 0)

La prueba comprueba tanto el tipo de excepción como parte de su mensaje. De esta forma se documenta el comportamiento esperado y se evita que el programa falle de manera inesperada.

8. Ejemplo completo de ciclo de trabajo

Cuando desarrolles una nueva función, puedes seguir esta secuencia:

  1. Define el comportamiento: escribe qué recibe la función y qué debe devolver.
  2. Prepara casos: incluye al menos un caso normal y otro que pueda revelar un error.
  3. Implementa lo mínimo: crea la función y ejecútala con pytest.
  4. Lee el informe: distingue entre una prueba fallida y un error de configuración o de sintaxis.
  5. Corrige y repite: cambia una cosa cada vez y vuelve a ejecutar todas las pruebas.
  6. Amplía la cobertura: añade casos cuando descubras una situación que no estaba contemplada.

Este ciclo conecta la programación con la depuración. Una prueba fallida no es un fracaso: es información concreta sobre una diferencia entre lo que el código hace y lo que se había especificado.

9. Buenas prácticas y errores frecuentes

Situación Por qué ocurre Cómo evitarlo
Instalar paquetes sin entorno virtual Las dependencias se mezclan entre proyectos. Crear .venv, activarlo y usar python -m pip.
Guardar pruebas junto al código Cuesta distinguir qué se ejecuta en la aplicación y qué la comprueba. Usar una carpeta tests/ y nombres test_*.py.
Probar solo un caso Un ejemplo puede ocultar errores en cero, negativos o límites. Diseñar varios casos representativos.
Usar print() en vez de una aserción La salida hay que revisarla manualmente y no expresa el resultado esperado. Comparar con assert.
Mezclar input() con la lógica La función depende del teclado y es difícil de repetir. Separar entrada, procesamiento y salida.
Ejecutar pytest desde otra carpeta Las importaciones o la detección de archivos pueden fallar. Ejecutarlo desde la raíz del proyecto.
Confundir failed con error No se distingue un resultado incorrecto de un problema de ejecución. Leer el traceback y comprobar primero sintaxis e importaciones.
Subir .venv al repositorio Se versionan muchos archivos dependientes del equipo. Añadir .venv/ a .gitignore.

No uses sudo para instalar pytest en este proyecto

Instalar paquetes globalmente puede romper la configuración de Python del sistema y ocultar qué dependencias necesita la aplicación. Trabaja con el entorno virtual activado. Si tu equipo usa una configuración distinta, sigue las instrucciones del centro o consulta a quien lo administre.

10. Resumen

  • Las pruebas automatizadas permiten comprobar el comportamiento esperado cada vez que se modifica el código.
  • Las funciones con una responsabilidad clara, parámetros explícitos y un valor de retorno son más fáciles de probar.
  • Un entorno virtual aísla las dependencias del proyecto y requirements.txt permite reproducirlas.
  • pytest descubre archivos y funciones de prueba siguiendo convenciones de nombres y utiliza assert para comprobar resultados.
  • pytest.mark.parametrize evita duplicar código al probar muchos casos, y pytest.raises comprueba errores esperados.
  • El informe de pytest orienta la depuración: hay que leerlo, corregir y volver a probar.

Idea clave

Una persona programadora profesional no espera al final para comprobar su programa. Escribe una parte pequeña, define qué resultado espera, la prueba, interpreta el informe y continúa. Probar mientras aprendes te ayuda a entender mejor las funciones y a construir programas más fiables.

11. Para seguir practicando

  • Escribe es_par(numero) y pruébala con 0, números positivos y negativos.
  • Crea una función que convierta grados Celsius a Fahrenheit y añade varios casos con pytest.mark.parametrize.
  • Diseña una función que rechace una lista vacía y comprueba la excepción con pytest.raises.

Bibliografía y fuentes

Presentación