Saltar a contenido

Práctica 1.6: Uso de pytest

P6 - Funciones y pruebas unitarias con pytest

Partimos del entorno de P5. Separar una función de la entrada y la salida permite probar la lógica con datos controlados. Una prueba fallida aporta información: no es un motivo para ocultar el error ni para cambiar la prueba sin analizarlo.

1. Objetivos

  • Definir funciones reutilizables con parámetros y return.
  • Organizar código en src/ y pruebas en tests/.
  • Escribir aserciones con assert, parametrizar casos y comprobar excepciones.
  • Ejecutar pytest desde el terminal y desde VS Code e interpretar su informe.

2. Relación con RA1 y criterios de evaluación

Esta práctica apoya el RA1, especialmente CE b y CE c, mediante la creación del proyecto y el uso del IDE. Las funciones y las pruebas son una herramienta de desarrollo para demostrar ese trabajo, no un CE adicional.

3. Conceptos previos

Una función es un bloque de código reutilizable. Favorece dos ideas importantes:

  • Reutilización: la misma función puede llamarse varias veces y desde diferentes partes del programa.
  • Modularización: un programa complejo se divide en partes pequeñas que se pueden entender, mantener y probar por separado.

Una definición de función incluye def, un nombre, parámetros opcionales, dos puntos, un bloque indentado y, cuando corresponda, una sentencia return:

def suma(numero1, numero2):
    """Devuelve la suma de dos números."""
    return numero1 + numero2

Las pruebas unitarias son pequeños programas que comprueban una unidad de código, normalmente una función. Se ejecutan de forma aislada y repetible. En proyectos profesionales pueden ejecutarse automáticamente después de cada cambio mediante integración continua; en esta práctica las ejecutarás desde el terminal y desde VS Code.

4. Pasos a seguir

4.1. Crear una función

En src/main.py escribe:

def suma(numero1, numero2):
    """Devuelve la suma de dos números."""
    return numero1 + numero2


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


def main():
    print(f"La suma de 3 + 2 es {suma(3, 2)}")


if __name__ == "__main__":
    main()

La función se puede reutilizar con print(suma(3, 2)), print(suma(25, 40)) o total = 100 + suma(25, 40). No mezcles input() con la lógica que quieres probar.

También puedes mostrar el resultado con distintas formas de construir el texto:

print("La suma de 3 + 2 es {}".format(suma(3, 2)))
print("La suma de", 3, "+", 2, "es", suma(3, 2))
print("La suma de " + str(3) + " + " + str(2) + " es " + str(suma(3, 2)))

texto = "La suma de "
texto += str(3) + " + " + str(2)
texto += " es " + str(suma(3, 2))
print(texto)

print(f"La suma de 3 + 2 es {suma(3, 2)}")

Estas variantes retoman los contenidos de entrada, conversión y formato de cadenas trabajados en P2. La prueba, sin embargo, debe comprobar el retorno de suma, no comparar un texto mostrado por pantalla.

4.2. Escribir y ejecutar la primera prueba

En tests/test_main.py escribe:

from src.main import dividir, suma


def test_suma_dos_numeros():
    resultado = suma(2, 3)
    assert resultado == 5


def test_suma_casos_sencillos():
    assert suma(0, 0) == 0
    assert suma(100, -100) == 0

Con el entorno activo y desde la raíz del proyecto:

(.venv) python -m pytest
(.venv) python -m pytest -v

Los archivos test_*.py y las funciones test_* se descubren automáticamente. Si la importación falla, revisa la raíz desde la que ejecutas pytest y la estructura de carpetas. Puedes añadir __init__.py vacío en src/ y tests/ si tu configuración del IDE lo necesita. Otra alternativa, especialmente si usas una carpeta src separada, es crear pytest.ini en la raíz:

[pytest]
pythonpath = ./src

En ese caso, el import de la prueba puede escribirse como from main import suma. Elige una de las dos configuraciones y mantenla igual en todo el proyecto; no mezcles rutas de importación sin comprobarlas.

Recuerda estas reglas al escribir pruebas:

  • importa la función desde el módulo donde está definida;
  • nombra las funciones de prueba con el prefijo test_;
  • separa cada comportamiento que quieras comprobar en una prueba clara;
  • utiliza assert para expresar el resultado esperado;
  • deja que pytest capture y explique los fallos en lugar de ocultarlos.

4.3. Parametrizar y probar errores

Amplía tests/test_main.py:

import pytest

from src.main import dividir, suma


@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_suma_varios_casos(primer_sumando, segundo_sumando, esperado):
    assert suma(primer_sumando, segundo_sumando) == esperado


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

Prueba casos normales, límites, negativos y errores esperados. Para observar un fallo controlado, cambia temporalmente una expectativa, ejecuta pytest, lee el informe y restáurala. No cambies el test para esconder un error del código.

4.4. Ejecutar desde VS Code

Selecciona el intérprete de .venv, abre la vista Testing, configura pytest si VS Code lo solicita y ejecuta una prueba o toda la carpeta. Comprueba también el botón de depuración. El terminal y el IDE deben usar el mismo entorno.

5. Comprobaciones de finalización

  • Todas las pruebas pasan con python -m pytest -v.
  • Se ha visto al menos un caso parametrizado y una excepción esperada.
  • El informe permite distinguir passed, failed y error.
  • Una prueba se ejecuta desde la vista Testing de VS Code.

6. Ejercicios y entregables

6.1. Comparar números

Crea compara_numeros.py y test_compara_numeros.py. La función recibe dos números y devuelve el mayor, o 0 si son iguales. Prueba números positivos, negativos e iguales.

6.2. Factorial desde el IDE

Crea calcula_factorial.py y sus pruebas. Para entradas de 0 a 10 devuelve el factorial; para cualquier otro valor devuelve -1. Recuerda que 0! = 1. Ejecuta las pruebas desde la vista Testing, provoca un error en el código y documenta la salida antes de corregirlo.

Entrega los módulos, los archivos de prueba, la estructura del proyecto y capturas del terminal y de VS Code con una ejecución correcta y otra fallida.

Fuentes y referencias