Módulo 2: Tu primer pipeline — correr pytest en CI
1. Presentación del módulo: de tu máquina al pipeline que corre pytest solo
Descripción
En el módulo 1 armamos el porqué. Viste que "en mi máquina funciona" no es una garantía sino una coincidencia afortunada, que el momento más barato para atrapar un error es apenas se escribe, y que un pipeline de integración continua existe para una sola cosa: correr tus tests automáticamente en cada cambio, sin depender de que alguien se acuerde. Esa idea ya la tienes. Lo que no tienes todavía es el archivo que la hace realidad.
Este módulo es ese archivo. Al terminarlo vas a haber escrito, entendido línea por línea, un workflow de GitHub Actions —un archivo de texto llamado tests.yml que vive en .github/workflows/— que le dice a GitHub: "cada vez que alguien haga push a este repositorio, arranca una máquina limpia, trae el código, instala Python y las dependencias, y corre pytest". Es el primer pipeline real de la guía, el más simple que hace su trabajo, y una vez que lo entiendas, todos los módulos que siguen (la matriz, el caché, las puertas de cobertura) son variaciones sobre este mismo esqueleto.
Vamos a trabajar sobre Reservo, el sistema de reservas de salas que ya conoces de las guías hermanas, con su suite de tests ya escrita. El giro del módulo es este: la misma suite que corrías en tu terminal con python3 -m pytest ahora va a correr sola, en la nube, en cada push. No cambia qué se prueba —eso ya está hecho—; cambia quién aprieta el botón y cuándo. La respuesta pasa de "yo, cuando me acuerdo" a "el pipeline, siempre".
Conexión con el módulo: esta es la lección-mapa. No escribimos el workflow completo todavía (eso empieza en la lección 2) ni tocamos GitHub de verdad; aquí construimos el vocabulario y el modelo mental que el resto del módulo da por sabidos. Las lecciones 2 a 6 arman el workflow pieza por pieza —la anatomía del YAML, los disparadores, los steps que preparan el terreno, instalar dependencias, y correr pytest con su exit code—. La lección 7 te enseña a leer el log y a colgar el badge. La lección 8 junta todo en un tests.yml completo para Reservo, con la corrida local que prueba que hace lo que dice.
La diferencia entre acordarse de regar las plantas y un riego automático
Imagina que tienes plantas en casa. Necesitan agua cada dos o tres días o se mueren. Tienes dos formas de mantenerlas vivas.
La primera es acordarte tú. Funciona los primeros días, cuando la intención está fresca. Pero un fin de semana sales de viaje, o tienes una semana pesada, o simplemente se te pasa, y para cuando te acuerdas, la planta está mustia. El problema no es que no sepas regar —lo sabes perfectamente—; el problema es que el sistema depende de tu memoria, y la memoria falla justo cuando estás más ocupado. Y lo peor: cuando fallas, no hay ninguna alarma. La planta no te avisa que tiene sed; solo la encuentras marchita días después.
La segunda forma es un riego automático: un temporizador conectado a una manguera con goteo. Lo configuras una vez —"cada dos días, a las siete de la mañana, treinta segundos"— y a partir de ahí riega solo, estés o no estés, te acuerdes o no. No riega mejor que tú; riega igual que tú, pero sin depender de que estés presente y atento. Configurarlo cuesta una tarde; después, la planta simplemente vive.
Correr los tests a mano es acordarte de regar. Sabes hacerlo —python3 -m pytest, lo tienes en los dedos— pero el sistema depende de que te acuerdes, y de que te acuerdes justo antes de compartir el código, que es cuando más presión tienes y más fácil se te pasa. Un pipeline de CI es el riego automático: lo configuras una vez, con el archivo tests.yml, y a partir de ahí los tests se corren solos en cada push, estés o no estés, te acuerdes o no. No prueban mejor que tú; prueban igual que tú, pero sin depender de tu memoria. Y cuando algo falla, a diferencia de la planta marchita, el pipeline sí te avisa: el push se pinta de rojo. Todo este módulo trata de escribir ese temporizador.
Qué es un workflow, en una frase
Antes de meternos, quedémonos con una definición que vas a poder repetir de memoria:
Un workflow es un archivo de texto que le dice a GitHub cuándo correr algo, dónde correrlo, y qué correr.
Eso es todo. No es un programa que escribes en un lenguaje nuevo ni una herramienta que instalas; es un archivo .yml que pones en una carpeta específica de tu repositorio (.github/workflows/), y GitHub lo lee y lo obedece. Cuando ocurre el evento que declaraste como disparador —por ejemplo, un push—, GitHub arranca una máquina virtual limpia en sus servidores, ejecuta los pasos que listaste, y te reporta el resultado. Esa máquina se llama un runner, y es, para todos los efectos, una computadora recién formateada: no tiene tu código, no tiene tus dependencias, no tiene nada tuyo. Tu workflow es la lista de instrucciones que la convierten, desde cero, en una máquina capaz de correr tu suite.
Fíjate en que las tres piezas de la definición —cuándo, dónde, qué— son exactamente las que vas a escribir:
- Cuándo: el bloque
on:del YAML. "Corre esto en cada push y en cada pull request." Es la lección 3. - Dónde: la clave
runs-on:. "Corre en una máquina Ubuntu limpia." Es parte de la lección 2, y el terreno lo preparan los steps de la lección 4. - Qué: la lista de
steps:. "Trae el código, instala Python, instala las dependencias, corre pytest." Son las lecciones 4, 5 y 6.
Todo el módulo es aprender a llenar esas tres respuestas con precisión.
Ejemplo trabajado: el workflow entero, de un vistazo
No lo escribas todavía —es la foto de a dónde vamos, no el trabajo de hoy—, pero míralo con atención, porque es el módulo completo en catorce líneas. Este es el tests.yml que vas a entender por dentro al terminar:
# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- run: python -m pip install --upgrade pip
- run: pip install -r requirements.txt
- run: pytest
Léelo como las tres preguntas, que ya casi se leen solas aunque no sepas YAML todavía:
name: tests— cómo se va a llamar este workflow en la interfaz de GitHub. Cosmético pero útil: es la etiqueta que verás en la pestaña "Actions".on: [push, pull_request]— el cuándo. Corre en cada push y en cada pull request. Esta línea es lo que hace todo "automático".jobs:→test:→runs-on: ubuntu-latest— el dónde. Define un trabajo llamadotestque corre en una máquina Ubuntu recién formateada.- Los cinco
steps— el qué, en orden: (1)checkouttrae tu código al runner; (2)setup-pythoninstala Python 3.14; (3) actualizapip; (4) instala tus dependencias desderequirements.txt; (5) correpytest.
Ese último paso, run: pytest, es exactamente el comando que tú escribes en tu terminal. No es una versión especial de pytest para la nube ni una magia distinta: es el mismo pytest, corriendo la misma suite, en una máquina que tu workflow acaba de preparar para que sea igual a la tuya. Ese es el corazón de la idea, y por eso podemos mostrártelo de verdad: cuando lleguemos a la lección 6, vas a correr pytest en tu propia máquina, ver la salida real, y entender que es idéntica a la que el CI produciría.
Qué corre de verdad y qué es "así se vería" (la regla de honestidad del módulo)
Hay una distinción que quiero que tengas clara desde ahora, porque es la que hace este módulo honesto en vez de mágico.
La corrida de pytest se ejecuta de verdad. Toda salida de pytest que veas en estas lecciones —el 11 passed, el 1 failed, el diff de un assert roto, el exit code— se produjo corriendo la suite de Reservo de verdad, en local, con Python 3.14.0 y pytest 9.1.1. Cuando digo "qué esperar" y te muestro un bloque verde, ese bloque salió de una terminal real. Puedes reproducirlo en tu máquina y obtener lo mismo.
El workflow de CI es contenido que se explica, no un CI que ejecutamos. No tenemos un runner de GitHub corriendo mientras escribes estas líneas, y no vamos a fingir que sí. El archivo tests.yml lo escribimos y lo desarmamos pieza por pieza; el formato del log que un runner produciría te lo mostramos como ilustración honesta —"así se vería en la pestaña Actions"—, con la salida de pytest real incrustada donde iría. La razón por la que esto funciona sin trampa es justamente la del párrafo anterior: lo que el CI le hace a tu suite es lo mismo que le hace tu máquina. El CI arranca una computadora, la prepara para que sea como la tuya, y corre pytest. Por eso podemos correr pytest en local, mostrarte la salida verdadera, y decirte con toda seguridad "esto es lo que el CI vería", sin haber levantado un CI.
Guárdate esta regla, porque la vas a ver aplicada en cada lección: pytest de verdad, workflow explicado. Es la diferencia entre aprender cómo funciona una cosa y que te la vendan.
Por qué GitHub Actions (y qué son las alternativas)
Vamos a usar GitHub Actions como la plataforma de CI de toda la guía, y conviene decir por qué, para que no lo tomes como la única opción del mundo.
GitHub Actions es el sistema de CI/CD integrado en GitHub. Si tu código ya vive en GitHub —como el de la enorme mayoría de proyectos hoy—, no tienes que instalar ni contratar nada aparte: pones un archivo .yml en .github/workflows/ y ya tienes pipeline. Esa cercanía —el CI vive en el mismo lugar que el código— es la razón de que sea, con diferencia, la plataforma más común para proyectos de código abierto y para muchísimos equipos, y por eso es la que aprendes aquí. Lo que practiques se aplica directo a un repositorio real tuyo.
Pero no es la única. GitLab CI/CD hace lo mismo con un archivo .gitlab-ci.yml; CircleCI, Jenkins, Azure Pipelines y otros cubren el mismo territorio. Cambian los nombres de las claves y algunos detalles, pero el modelo mental es idéntico en todas: un archivo declara cuándo correr, dónde correr, y qué pasos ejecutar; una máquina limpia se levanta, sigue los pasos, corre tus tests, y reporta verde o rojo según el exit code. Si mañana te cambias a GitLab, no vas a reaprender el concepto, solo la sintaxis. Aprender uno a fondo —y vamos a aprender GitHub Actions a fondo— es aprender el patrón que comparten todos.
Qué pasa entre el push y el veredicto
Antes de cerrar, vale la pena ver la película completa de una corrida, de principio a fin, aunque los detalles de cada paso lleguen en las lecciones siguientes. Tener la secuencia en la cabeza te ayuda a entender por qué el pipeline tarda "unos minutos" y no es instantáneo: hay una máquina que nace, trabaja y muere en cada corrida.
1. Haces git push.
│
2. GitHub ve el evento y revisa .github/workflows/.
Encuentra tests.yml y comprueba: ¿su "on:" incluye push? Sí.
│
3. GitHub arranca un runner: una máquina virtual Ubuntu limpia,
recién formateada, sin tu código y sin tus dependencias.
│
4. El runner ejecuta los steps en orden, de arriba a abajo:
checkout → trae tu código
setup-python → instala Python 3.14
pip install → instala tus dependencias
pytest → corre la suite y devuelve un exit code
│
5. GitHub lee el exit code del último step:
0 → job verde ✓
≠0 → job rojo ✗
│
6. El runner se destruye por completo (la máquina desaparece).
El resultado —verde o rojo— queda en la pestaña Actions y en el badge.
Tres cosas que esta película te deja claras y que valen para todo el módulo:
- La máquina es efímera. Nace en el paso 3 y muere en el paso 6. No se guarda nada entre corridas: la siguiente vez, otra máquina limpia empieza desde cero. Eso es lo que da la reproducibilidad —ninguna corrida arrastra basura de la anterior— y es la razón de que cada corrida tenga que reconstruir el entorno con los steps, en vez de "recordarlo".
- El grueso del tiempo se va en preparar el terreno, no en probar. Para Reservo,
pytesttarda centésimas de segundo, pero arrancar la máquina, traer el código, instalar Python e instalar dependencias toma la mayor parte de esos "un par de minutos". Por eso el módulo 5 se dedica a acelerar justamente esa preparación (cacheando dependencias): la suite ya es rápida; lo lento es el montaje. - Todo el veredicto se reduce a un número. El paso 5 es toda la inteligencia del CI: leer el exit code de pytest. El módulo 1 te dijo que el CI avisa; aquí ves el mecanismo exacto —un
0o un no-0— y la lección 6 lo desarma por completo.
Errores comunes
Creer que el CI "prueba tu código por ti" o encuentra bugs solo (de expectativa mágica). Qué pasa: alguien monta un pipeline esperando que descubra problemas que sus tests no cubren, y se decepciona cuando el CI pasa en verde sobre código con bugs. Por qué pasa: "integración continua" suena a que la inteligencia está en la plataforma. No lo está. El CI corre los tests que tú escribiste, ni uno más; si tu suite no cubre un caso, el CI tampoco lo va a cubrir. Cómo detectarlo: si esperas que el pipeline te avise de un bug para el que no hay test, tienes el modelo al revés. Cómo corregirlo: recuerda que el CI automatiza el cuándo y el dónde de correr tu suite, no el qué pruebas. La calidad de la protección la pone tu suite (eso es la guía de fundamentos); el CI solo garantiza que esa suite se corra siempre.
Pensar que el runner tiene tu entorno ya montado (de máquina compartida). Qué pasa: alguien escribe un workflow que corre pytest directo, sin traer el código ni instalar nada, imaginando que el runner es "como mi máquina pero en la nube". Falla con "no such file" o "no module named pytest". Por qué pasa: uno da por hecho que el CI hereda su entorno local. No lo hereda: el runner arranca vacío, recién formateado, sin tu código y sin tus paquetes. Cómo detectarlo: si tu workflow no tiene un step de checkout y otro de instalar dependencias, le falta el terreno. Cómo corregirlo: todo workflow empieza preparando la máquina —traer el código, poner Python, instalar dependencias— antes de correr nada. Son justo los steps de las lecciones 4 y 5, y existen precisamente porque el runner no sabe nada de ti.
Confundir "workflow verde" con "código desplegado" (de alcance). Qué pasa: alguien ve el badge verde y cree que su cambio ya está en producción, o que el CI "publicó" algo. Por qué pasa: se mezclan dos ideas —integración continua (CI) y despliegue continuo (CD)— que suelen ir juntas pero no son lo mismo. Cómo detectarlo: si crees que este workflow sube tu app a un servidor, revisa qué steps tiene: solo corre tests. Cómo corregirlo: en esta guía el foco es el CI de tests —correr tu suite en cada cambio—. El "CD", el despliegue automático a producción, es otra etapa que se menciona como concepto pero no montamos aquí. Verde significa "tus tests pasaron en una máquina limpia", que es muchísimo, pero no significa "está en producción".
Ejercicios
Ejercicio 1 — Traduce a las tres preguntas. Un compañero te describe en palabras lo que quiere de su pipeline: "Que cada vez que yo suba código, se prenda una computadora nueva con Ubuntu, ponga Python, instale las librerías del proyecto y corra la suite de tests." Sin escribir YAML todavía, mapea cada parte de esa frase a una de las tres preguntas de un workflow (cuándo, dónde, qué) y di cuál de ellas incluye varias cosas.
Ver solución
- Cuándo: "cada vez que yo suba código" → el disparador, el
on:(un push). Es la lección 3. - Dónde: "una computadora nueva con Ubuntu" →
runs-on: ubuntu-latest. Es la máquina limpia, el runner. - Qué: "ponga Python, instale las librerías del proyecto y corra la suite de tests" → los
steps, y esta es la que incluye varias cosas: poner Python (setup-python, lección 4), instalar dependencias (pip install, lección 5), y correr la suite (pytest, lección 6). Falta una que el compañero no mencionó pero es indispensable: traer el código al runner (checkout), porque la máquina nueva no lo tiene.
La lección aquí es que el qué casi siempre es una lista de pasos en orden, no una sola acción, y que uno de esos pasos —traer el código— es tan obvio que se olvida, aunque sin él no hay nada que probar.
Ejercicio 2 — Riego automático o memoria. Para cada una de estas tres situaciones, di si describe el problema del "acordarse de regar" (correr tests a mano) o la solución del "riego automático" (CI), y por qué: (a) "el viernes antes del festivo largo nadie corrió los tests y el lunes el bug ya estaba en la rama de todos"; (b) "cada push aparece marcado con una palomita verde o una tache roja sin que nadie haga nada"; (c) "sé perfectamente correr la suite, el problema es que se me olvida justo cuando ando apurado".
Ver solución
- (a) es el problema de la memoria: el sistema dependía de que alguien se acordara, y justo cuando había más prisa y distracción (un viernes antes de festivo), nadie lo hizo. Como la planta marchita, además no hubo alarma hasta que fue tarde (el lunes).
- (b) es el riego automático funcionando: la palomita/tache aparece "sin que nadie haga nada" porque el pipeline se dispara solo en cada push. Ese "sin que nadie haga nada" es exactamente lo que compra el CI.
- (c) es la descripción más pura del problema de la memoria, y la razón de fondo por la que existe el CI: el conocimiento no falta ("sé correr la suite"), falla la disciplina de ejecutarla siempre, y falla peor bajo presión. El CI no te enseña a probar; te quita de encima la carga de acordarte.
El hilo: el CI no te vuelve mejor probando, te libera de depender de tu memoria para hacerlo. Igual que el temporizador no riega mejor, riega siempre.
Ejercicio 3 — Verdad o "así se vería". Según la regla de honestidad del módulo, clasifica cada uno de estos como "se ejecuta de verdad en local" o "es contenido que se explica / así se vería": (a) el bloque 11 passed in 0.03s que verás en la lección 6; (b) una captura del log de la pestaña Actions de GitHub con palomitas verdes por step; (c) el diff assert 5625 == 6000 de un test roto; (d) el archivo tests.yml con sus steps.
Ver solución
- (a) el
11 passed in 0.03s: se ejecuta de verdad en local. Toda salida de pytest de esta guía sale de correr la suite real con pytest 9.1.1. - (b) el log de la pestaña Actions con palomitas: así se vería / contenido. No levantamos un runner de GitHub; te mostramos el formato del log como ilustración honesta.
- (c) el diff
assert 5625 == 6000: se ejecuta de verdad en local. Es la salida real de pytest cuando un test de Reservo falla (lo provocamos rompiendo el código a propósito y corriendo pytest de verdad). - (d) el archivo
tests.yml: contenido que se explica. Es un archivo que escribimos y desarmamos, no algo que "ejecutamos" en el sentido de levantar un CI.
La regla, en una línea: la salida de pytest es real; el workflow y su log son contenido explicado. Y funciona porque el CI le hace a tu suite lo mismo que tu máquina, así que la salida local es la que el CI vería.
Resumen y siguiente paso
En esta lección cerraste la brecha entre la idea del módulo 1 —"el CI corre tus tests en cada cambio"— y el archivo concreto que la cumple. Un workflow es un archivo de texto que le dice a GitHub cuándo correr algo (el disparador), dónde correrlo (una máquina limpia, el runner), y qué correr (los pasos: traer el código, poner Python, instalar dependencias, correr pytest). Viste el tests.yml entero de un vistazo y aprendiste a leerlo como esas tres preguntas, y entendiste que el último paso —run: pytest— es exactamente el mismo comando que corres en tu terminal, sobre la misma suite: el CI no prueba distinto, prueba en una máquina que preparó para ser como la tuya.
Fijaste también la regla de honestidad que gobierna todo el módulo: la corrida de pytest se ejecuta de verdad en local (y por eso podemos pegarte salidas reales), mientras que el workflow es contenido que se explica y el log de CI se muestra como "así se vería". Y ubicaste GitHub Actions como la plataforma canónica —con GitLab CI y otras como equivalentes conceptuales del mismo patrón—.
Antes de avanzar deberías poder: definir un workflow en una frase; nombrar las tres preguntas que responde y qué clave del YAML corresponde a cada una; explicar por qué el runner arranca vacío y qué implica eso; y distinguir qué parte de este módulo se ejecuta de verdad y qué parte es ilustración honesta.
Lo que sigue es dejar de mirar la foto y desarmarla. En la lección 2 vas a abrir ese tests.yml y a entender cada línea: dónde vive el archivo, cómo funciona el YAML como formato (la indentación que sí importa), y las cuatro claves que estructuran todo —name, on, jobs, y dentro de un job runs-on y steps—. Al terminar esa lección, el archivo dejará de ser catorce líneas misteriosas y será una receta que puedes leer y modificar.
Recursos
- Entender GitHub Actions — la introducción oficial a los conceptos que nombramos aquí: workflow, evento, job, step, runner. Está en inglés; el vocabulario que aprendiste es el mismo que usa. El mejor punto de partida para el panorama completo.
- Documentación oficial de pytest — página de inicio — la referencia de la herramienta que el CI va a correr. La misma suite que corres en local es la que el pipeline ejecuta; este es el manual de la parte que sí se ejecuta de verdad.
- Continuous integration (documentación de GitHub Actions) — qué es la integración continua y cómo GitHub Actions la implementa, con la distinción CI/CD que mencionamos en los errores comunes. Útil para afianzar el "verde no es producción".