5. Primer script de ejecución automática de pruebas

5.1 Objetivo del tema

Hasta ahora ejecutamos las pruebas escribiendo comandos de pytest desde la terminal. En este tema construiremos un script de Python para centralizar esa ejecución.

El script permitirá ejecutar la suite con un comando único, agregar opciones, crear la carpeta de reportes y devolver un código de salida correcto cuando las pruebas pasan o fallan.

Objetivo práctico: crear un archivo run_tests.py que ejecute python -m pytest de forma automática, clara y repetible.

5.2 Por qué usar un script de ejecución

Un script de ejecución evita que cada persona recuerde comandos distintos. En una suite automatizada, esto ayuda a mantener consistencia.

Un buen script permite:

  • Ejecutar siempre pytest con el Python correcto.
  • Agregar opciones comunes sin escribirlas cada vez.
  • Crear carpetas necesarias antes de ejecutar.
  • Generar reportes con nombres predecibles.
  • Devolver un código de salida útil para otros procesos.

5.3 Comando base que vamos a automatizar

El comando base del curso será:

python -m pytest

Lo importante es no depender del comando directo pytest. Al usar python -m pytest, ejecutamos pytest con el intérprete de Python activo en el proyecto.

5.4 Crear un script mínimo

Crea o reemplaza el archivo run_tests.py en la raíz del proyecto:

import subprocess
import sys


def main():
    comando = [sys.executable, "-m", "pytest"]
    resultado = subprocess.run(comando, check=False)
    return resultado.returncode


if __name__ == "__main__":
    raise SystemExit(main())

Ejecuta el script:

python run_tests.py

sys.executable representa el Python que está ejecutando el script. Así evitamos llamar a otro Python instalado en el sistema.

5.5 Entender subprocess.run

subprocess.run permite ejecutar un comando externo desde Python. En este caso, ejecutamos pytest como si lo escribiéramos en la terminal.

La lista:

[sys.executable, "-m", "pytest"]

equivale a ejecutar:

python -m pytest

La opción check=False evita que Python lance una excepción si las pruebas fallan. En su lugar, podemos leer el código de salida.

5.6 Códigos de salida

Cuando pytest termina, devuelve un número. Ese número permite saber si la ejecución fue exitosa o no.

  • 0: todas las pruebas pasaron.
  • 1: alguna prueba falló.
  • 2: la ejecución fue interrumpida.
  • 4: hubo un error de uso del comando.
  • 5: no se encontraron pruebas.

El script devuelve ese mismo código con raise SystemExit(main()). Esto será importante cuando otro proceso necesite saber si la suite pasó o falló.

5.7 Agregar salida detallada

Modifica el comando para que use la opción -v:

comando = [sys.executable, "-m", "pytest", "-v"]

Ahora la ejecución mostrará el nombre de cada prueba. El script completo queda así:

import subprocess
import sys


def main():
    comando = [sys.executable, "-m", "pytest", "-v"]
    resultado = subprocess.run(comando, check=False)
    return resultado.returncode


if __name__ == "__main__":
    raise SystemExit(main())

5.8 Crear la carpeta reports automáticamente

Si vamos a generar reportes, conviene que el script cree la carpeta antes de ejecutar la suite.

from pathlib import Path


REPORTS_DIR = Path("reports")
REPORTS_DIR.mkdir(exist_ok=True)

Path("reports").mkdir(exist_ok=True) crea la carpeta si no existe y no falla si ya fue creada.

5.9 Generar un reporte HTML desde el script

Agrega las opciones de pytest-html al comando:

comando = [
    sys.executable,
    "-m",
    "pytest",
    "-v",
    "--html=reports/reporte.html",
    "--self-contained-html",
]

Así, cada ejecución creará o reemplazará el archivo reports/reporte.html.

5.10 Script con reporte automático

El archivo run_tests.py puede quedar así:

import subprocess
import sys
from pathlib import Path


REPORTS_DIR = Path("reports")


def main():
    REPORTS_DIR.mkdir(exist_ok=True)
    comando = [
        sys.executable,
        "-m",
        "pytest",
        "-v",
        "--html=reports/reporte.html",
        "--self-contained-html",
    ]
    resultado = subprocess.run(comando, check=False)
    return resultado.returncode


if __name__ == "__main__":
    raise SystemExit(main())

Ejecuta:

python run_tests.py

5.11 Agregar argumentos al script

Ahora haremos que el script acepte opciones desde la terminal. Usaremos argparse, un módulo incluido en Python.

Primero importamos:

import argparse

Luego creamos una función para leer argumentos:

def leer_argumentos():
    parser = argparse.ArgumentParser()
    parser.add_argument("--rapido", action="store_true")
    parser.add_argument("--reporte", action="store_true")
    return parser.parse_args()

5.12 Ejecutar con o sin reporte

Con argumentos, podemos generar reporte solo cuando lo pedimos:

if args.reporte:
    REPORTS_DIR.mkdir(exist_ok=True)
    comando.extend([
        "--html=reports/reporte.html",
        "--self-contained-html",
    ])

Así el comando simple:

python run_tests.py

ejecuta la suite sin reporte, y este comando:

python run_tests.py --reporte

ejecuta la suite y genera el archivo HTML.

5.13 Seleccionar pruebas rápidas

En temas posteriores clasificaremos pruebas con marcadores. Por ahora dejaremos preparado un argumento --rapido para ejecutar pruebas que no estén marcadas como lentas.

if args.rapido:
    comando.extend(["-m", "not lento"])

Más adelante agregaremos el marcador lento en la configuración del proyecto. La idea es que el script ya tenga una forma clara de ejecutar una suite reducida.

5.14 Script completo con argumentos

Reemplaza run_tests.py por esta versión:

import argparse
import subprocess
import sys
from pathlib import Path


REPORTS_DIR = Path("reports")


def leer_argumentos():
    parser = argparse.ArgumentParser(description="Ejecuta la suite automatizada")
    parser.add_argument("--rapido", action="store_true", help="Excluye pruebas marcadas como lentas")
    parser.add_argument("--reporte", action="store_true", help="Genera un reporte HTML")
    return parser.parse_args()


def construir_comando(args):
    comando = [sys.executable, "-m", "pytest", "-v"]

    if args.rapido:
        comando.extend(["-m", "not lento"])

    if args.reporte:
        REPORTS_DIR.mkdir(exist_ok=True)
        comando.extend([
            "--html=reports/reporte.html",
            "--self-contained-html",
        ])

    return comando


def main():
    args = leer_argumentos()
    comando = construir_comando(args)
    resultado = subprocess.run(comando, check=False)
    return resultado.returncode


if __name__ == "__main__":
    raise SystemExit(main())

5.15 Probar las opciones del script

Ejecuta la suite completa:

python run_tests.py

Ejecuta la suite con reporte:

python run_tests.py --reporte

Ejecuta la suite rápida:

python run_tests.py --rapido

Ejecuta la suite rápida y genera reporte:

python run_tests.py --rapido --reporte

5.16 Mostrar el comando antes de ejecutarlo

Para que el script sea más transparente, podemos imprimir el comando final:

print("Ejecutando:", " ".join(comando))

Agrega esa línea antes de subprocess.run:

def main():
    args = leer_argumentos()
    comando = construir_comando(args)
    print("Ejecutando:", " ".join(comando))
    resultado = subprocess.run(comando, check=False)
    return resultado.returncode

Esto ayuda a diagnosticar qué opciones está usando realmente la ejecución.

5.17 Evitar lógica innecesaria en el script

Un script de ejecución debe ser útil, pero no debe convertirse en una aplicación compleja. Conviene mantenerlo simple y enfocado.

Evita agregarle responsabilidades como:

  • Crear datos de negocio complejos.
  • Modificar archivos de la aplicación antes de probar.
  • Ocultar errores de pytest.
  • Ignorar fallas para que el resultado parezca exitoso.
  • Duplicar configuración que ya está en pytest.ini.

5.18 Actualizar el README

Agrega al README.md una sección para documentar el uso del script:

## Ejecutar la suite automatizada

python run_tests.py

## Ejecutar y generar reporte HTML

python run_tests.py --reporte

## Ejecutar suite rápida

python run_tests.py --rapido

La automatización también necesita documentación breve. Un comando útil pierde valor si nadie sabe cómo usarlo.

5.19 Problemas frecuentes

  • El script no encuentra pytest: activa el entorno virtual e instala las dependencias.
  • No se genera el reporte: revisa que esté instalado pytest-html.
  • El argumento --rapido muestra advertencias: configura el marcador lento en pytest.ini cuando empieces a usarlo.
  • El script devuelve error aunque se ejecutó: probablemente una prueba falló; revisa la salida de pytest.
  • El comando se ejecuta con otro Python: confirma que estás usando el entorno virtual correcto.

5.20 Ejercicio práctico

Agrega al script un nuevo argumento llamado --detener. Cuando se use, debe agregar la opción -x al comando de pytest para detener la ejecución en la primera falla.

Luego prueba estos comandos:

python run_tests.py --detener
python run_tests.py --detener --reporte

5.21 Solución propuesta

Agrega el argumento en leer_argumentos:

parser.add_argument("--detener", action="store_true", help="Detiene la ejecución en la primera falla")

Luego agrega esta condición en construir_comando:

if args.detener:
    comando.append("-x")

La función completa puede quedar así:

def construir_comando(args):
    comando = [sys.executable, "-m", "pytest", "-v"]

    if args.rapido:
        comando.extend(["-m", "not lento"])

    if args.detener:
        comando.append("-x")

    if args.reporte:
        REPORTS_DIR.mkdir(exist_ok=True)
        comando.extend([
            "--html=reports/reporte.html",
            "--self-contained-html",
        ])

    return comando

5.22 Lista de verificación

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

  • Existe el archivo run_tests.py en la raíz del proyecto.
  • El script ejecuta pytest usando sys.executable, -m y pytest.
  • El script devuelve el código de salida real de pytest.
  • La opción --reporte genera un archivo HTML.
  • La opción --rapido agrega una selección de pruebas.
  • La opción --detener detiene la suite en la primera falla.
  • El README documenta los comandos principales.

5.23 Conclusión

En este tema creamos un primer script de ejecución automática para la suite de pruebas. El script centraliza el comando, usa el Python correcto, permite opciones simples y conserva el código de salida de pytest.

Este tipo de automatización reduce errores de ejecución y prepara el proyecto para flujos más avanzados. En el próximo tema trabajaremos con la configuración centralizada usando pyproject.toml y pytest.ini.