19. Automatizar verificaciones de salida, logs y mensajes de error

19.1 Objetivo del tema

Una prueba automatizada no siempre verifica solo valores de retorno. A veces necesitamos comprobar qué se imprime en pantalla, qué mensaje trae una excepción o qué registro queda en los logs.

En este tema usaremos herramientas de pytest para capturar salida por consola, validar mensajes de error y revisar logs de forma controlada.

Objetivo práctico: verificar salidas visibles y diagnósticos sin depender de una revisión manual.

19.2 Qué podemos verificar

En automatización de pruebas podemos verificar distintos tipos de salida:

  • Salida estándar: textos impresos con print.
  • Salida de error: mensajes escritos en stderr.
  • Excepciones: tipo de error y mensaje asociado.
  • Logs: registros generados con el módulo logging.
  • Mensajes al usuario: textos que explican una validación o falla.

19.3 Crear un módulo de consola

Crea app/consola.py:

def mostrar_resumen(nombre, total):
    print(f"Usuario: {nombre}")
    print(f"Total: {total}")


def mostrar_error(mensaje):
    print(f"ERROR: {mensaje}")

Estas funciones imprimen texto en la salida estándar.

19.4 Capturar salida con capsys

pytest ofrece la fixture capsys para capturar lo que se imprime durante una prueba.

from app.consola import mostrar_resumen


def test_mostrar_resumen_imprime_usuario_y_total(capsys):
    mostrar_resumen("Ana", 350)

    salida = capsys.readouterr()

    assert "Usuario: Ana" in salida.out
    assert "Total: 350" in salida.out

salida.out contiene lo impreso por salida estándar.

19.5 Ejecutar la prueba

Crea tests/test_consola.py con la prueba anterior y ejecuta:

python -m pytest tests/test_consola.py

La prueba pasa si los textos esperados fueron impresos.

19.6 Verificar líneas exactas

Si el orden importa, podemos dividir la salida en líneas:

def test_mostrar_resumen_imprime_lineas_esperadas(capsys):
    mostrar_resumen("Ana", 350)

    salida = capsys.readouterr()
    lineas = salida.out.strip().splitlines()

    assert lineas == [
        "Usuario: Ana",
        "Total: 350",
    ]

Esta prueba es más estricta: valida contenido y orden.

19.7 Capturar stderr

Si una función escribe en stderr, capsys también puede capturarlo. Modifica app/consola.py:

import sys


def mostrar_error_stderr(mensaje):
    print(f"ERROR: {mensaje}", file=sys.stderr)

Prueba:

from app.consola import mostrar_error_stderr


def test_mostrar_error_stderr_escribe_en_salida_de_error(capsys):
    mostrar_error_stderr("archivo no encontrado")

    salida = capsys.readouterr()

    assert salida.out == ""
    assert "ERROR: archivo no encontrado" in salida.err

19.8 Verificar mensajes de excepción

Con pytest.raises podemos comprobar el tipo de excepción y su mensaje.

def dividir(a, b):
    if b == 0:
        raise ValueError("No se puede dividir por cero")

    return a / b

Prueba:

import pytest


def test_dividir_con_divisor_cero_lanza_mensaje_claro():
    with pytest.raises(ValueError, match="No se puede dividir por cero"):
        dividir(10, 0)

match verifica que el mensaje de la excepción coincida con el texto indicado.

19.9 Capturar la excepción como objeto

También podemos capturar la excepción para revisar detalles:

def test_dividir_con_divisor_cero_permite_revisar_excepcion():
    with pytest.raises(ValueError) as error:
        dividir(10, 0)

    assert str(error.value) == "No se puede dividir por cero"

Esta forma es útil cuando queremos varias aserciones sobre el error.

19.10 Crear un módulo con logging

Crea app/procesos.py:

import logging


logger = logging.getLogger(__name__)


def procesar_pago(monto):
    if monto <= 0:
        logger.error("Monto inválido para pago: %s", monto)
        raise ValueError("El monto debe ser mayor a cero")

    logger.info("Pago procesado por monto: %s", monto)
    return True

El módulo registra mensajes informativos y errores.

19.11 Capturar logs con caplog

pytest ofrece caplog para inspeccionar logs generados durante una prueba.

import logging

from app.procesos import procesar_pago


def test_procesar_pago_valido_registra_mensaje_info(caplog):
    with caplog.at_level(logging.INFO):
        procesar_pago(100)

    assert "Pago procesado por monto: 100" in caplog.text

caplog.text contiene los mensajes capturados.

19.12 Verificar logs de error

Podemos combinar excepción y log:

import logging
import pytest

from app.procesos import procesar_pago


def test_procesar_pago_invalido_registra_error_y_lanza_excepcion(caplog):
    with caplog.at_level(logging.ERROR):
        with pytest.raises(ValueError, match="El monto debe ser mayor a cero"):
            procesar_pago(0)

    assert "Monto inválido para pago: 0" in caplog.text

La prueba valida tanto el error lanzado como el log generado.

19.13 Revisar registros individuales

caplog.records permite inspeccionar cada registro:

def test_procesar_pago_invalido_registra_nivel_error(caplog):
    with caplog.at_level(logging.ERROR):
        with pytest.raises(ValueError):
            procesar_pago(0)

    assert caplog.records[0].levelname == "ERROR"
    assert caplog.records[0].message == "Monto inválido para pago: 0"

Esto es más preciso que buscar texto en toda la salida.

19.14 No probar mensajes internos innecesarios

No todos los logs merecen una prueba. Conviene verificar mensajes cuando son parte del diagnóstico esperado, auditoría o comportamiento importante.

Evita pruebas demasiado frágiles que fallen por cambiar una palabra sin afectar el comportamiento real.

19.15 Mensajes claros en errores

Los mensajes de error deben ayudar a entender el problema. Por ejemplo:

raise ValueError("El monto debe ser mayor a cero")

Es mejor que:

raise ValueError("Error")

Las pruebas pueden ayudarnos a mantener mensajes útiles para usuarios o desarrolladores.

19.16 Parametrizar mensajes de error

Podemos probar varios casos inválidos con parametrización:

import pytest


@pytest.mark.parametrize("monto", [0, -10])
def test_procesar_pago_con_montos_invalidos_lanza_mensaje_claro(monto):
    with pytest.raises(ValueError, match="El monto debe ser mayor a cero"):
        procesar_pago(monto)

19.17 Verificar que no haya salida inesperada

A veces queremos comprobar que una función no imprime nada:

def sumar(a, b):
    return a + b


def test_sumar_no_imprime_salida(capsys):
    assert sumar(2, 3) == 5

    salida = capsys.readouterr()

    assert salida.out == ""
    assert salida.err == ""

Esto puede servir para detectar impresiones de depuración que quedaron por error.

19.18 Problemas frecuentes

  • La salida no aparece en capsys: llama a capsys.readouterr() después de ejecutar la función.
  • El log no se captura: revisa el nivel usado en caplog.at_level.
  • La prueba es frágil por texto exacto: usa in si solo importa una parte del mensaje.
  • El mensaje de excepción no coincide: recuerda que match usa expresión regular.
  • Se mezclan salidas de varias acciones: lee la salida después de cada acción relevante.

19.19 Ejercicio práctico

Crea app/importador.py con una función importar_usuario. La función debe:

  • Recibir un diccionario con nombre y email.
  • Imprimir Importando usuario: nombre.
  • Registrar un log informativo cuando el usuario se importa.
  • Lanzar ValueError con mensaje claro si falta el email.

Luego crea pruebas para salida por consola, log y mensaje de error.

19.20 Solución propuesta

Archivo app/importador.py:

import logging


logger = logging.getLogger(__name__)


def importar_usuario(usuario):
    if not usuario.get("email"):
        logger.error("Usuario sin email: %s", usuario.get("nombre"))
        raise ValueError("El usuario debe tener email")

    print(f"Importando usuario: {usuario['nombre']}")
    logger.info("Usuario importado: %s", usuario["email"])
    return True

Pruebas:

import logging
import pytest

from app.importador import importar_usuario


def test_importar_usuario_imprime_nombre(capsys):
    importar_usuario({"nombre": "Ana", "email": "ana@example.com"})

    salida = capsys.readouterr()

    assert "Importando usuario: Ana" in salida.out


def test_importar_usuario_registra_log_info(caplog):
    with caplog.at_level(logging.INFO):
        importar_usuario({"nombre": "Ana", "email": "ana@example.com"})

    assert "Usuario importado: ana@example.com" in caplog.text


def test_importar_usuario_sin_email_lanza_mensaje_claro(caplog):
    with caplog.at_level(logging.ERROR):
        with pytest.raises(ValueError, match="El usuario debe tener email"):
            importar_usuario({"nombre": "Ana"})

    assert "Usuario sin email: Ana" in caplog.text

Ejecuta:

python -m pytest tests/test_importador.py

19.21 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Sabes capturar salida estándar con capsys.
  • Sabes capturar salida de error con capsys.
  • Sabes verificar mensajes de excepción con pytest.raises.
  • Sabes capturar logs con caplog.
  • Validas mensajes importantes sin volver las pruebas innecesariamente frágiles.
  • La suite se ejecuta correctamente con python -m pytest.

19.22 Conclusión

En este tema automatizamos verificaciones sobre salida por consola, logs y mensajes de error. Estas pruebas ayudan a controlar diagnósticos importantes y mensajes que facilitan la comprensión de fallas.

En el próximo tema veremos cómo lograr pruebas determinísticas controlando fechas, aleatoriedad y dependencias externas.