Ir al contenido
wannacode

Pruebas en CI

En resumen: CI ejecuta las pruebas con cada cambio, en una máquina limpia, y no permite fusionar mientras algo esté en rojo. Una prueba que falla es una señal: un defecto, una regla cambiada, una prueba equivocada o un entorno roto. La más peligrosa es la prueba que falla a veces: la gente se acostumbra a volver a ejecutarla y se pierde el fallo real. La causa casi siempre es algo que la prueba no controla, como la hora.

Problema

Rojo por la noche, verde por la mañana

Todas las noches a las 00:30, hora de Madrid, el servidor ejecuta todas las pruebas. Tres noches seguidas falla la prueba «el código promocional vale en su último día». Por la mañana el programador la vuelve a ejecutar, y pasa.

«Falla a veces, vuelve a ejecutarla» se convirtió en costumbre. Un mes después nadie miraba el informe nocturno. Entonces falló una prueba real de un descuento, y nadie lo vio hasta que se quejó el primer cliente.

La idea

Integración continua

La integración continua (continuous integration, CI) es un acuerdo: con cada cambio, antes de que entre en el código compartido, un servidor toma el código en una máquina limpia, lo compila y ejecuta las pruebas. Si algo está en rojo, el cambio no se fusiona.

Aquí está la configuración para GitHub Actions, una de las herramientas más extendidas. Se ejecuta con cada push y cada solicitud de fusión (pull request):

.github/workflows/tests.yml
name: Pruebas
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
# Sin las pruebas de interfaz (marcadas con ui): van aparte, una vez por noche.
- run: mvn --batch-mode test -DexcludedGroups=ui

Las pruebas de componente, los escenarios y las pruebas de API de este módulo se ejecutan aquí. Las pruebas de interfaz van aparte, una vez por noche contra el entorno de pruebas: necesitan un navegador y una tienda en marcha y son demasiado lentas para cada cambio.

Más precisamente, para curiosos

La configuración se muestra pero no se ejecuta en el curso: necesita GitHub. Otras herramientas extendidas son GitLab CI, Jenkins y Azure Pipelines. La idea y los pasos son los mismos: obtener el código, preparar el lenguaje, ejecutar las pruebas, guardar el informe.

La regla «el rojo detiene la fusión» se configura aparte, en la protección de la rama principal (en GitHub: branch protection, required checks). Sin ella, CI es solo un consejo.

Pruébalo

El camino de un cambio

Ordena lo que pasa desde que se sube el cambio hasta la fusión.

  1. El programador sube el cambio y abre una solicitud de fusión (pull request)
  2. Las pruebas de componente
  3. Se compila el código
  4. Los escenarios y las pruebas de API
  5. El informe: qué pasó y qué falló
  6. CI toma el código en una máquina limpia
  7. La fusión se permite solo si todo está en verde

La idea

Por qué falla una prueba

El rojo en CI es una señal, no un veredicto. Hay cuatro causas, y cada una necesita algo distinto:

  • un defecto en el código: el cambio rompió algo. Se corrige el código. Esta es la regresión para la que existen las pruebas;
  • la regla cambió: la prueba enuncia el requisito viejo. La prueba cambia, pero según el requisito nuevo, no «hasta que pase»;
  • una prueba equivocada: la prueba comprueba algo que nadie pidió. Se corrige la prueba, y se avisa al autor;
  • el entorno: la base de datos no arrancó, la red es lenta. Se mira en la máquina, no en el producto.

Una cosa no es una causa: «vuelve a ejecutarla». Si la prueba pasa al segundo intento sin ningún cambio, el problema sigue ahí.

Pruébalo

¿Qué significa este rojo?

1. La prueba del envío gratis falla después de un cambio en la lógica del código promocional. La regla del envío no ha cambiado.
2. El dueño decidió que el umbral debe ser 60 €. El programador cambió el código, y la prueba de 50,00 € falla.
3. Todas las pruebas de API fallan con «connection refused», y ayer pasaban. El código no se ha tocado.
4. Una prueba falla una vez de cada diez ejecuciones, sin ningún cambio en el código.

La idea

Una prueba que falla a veces

Aquí está la prueba del principio. Un código promocional vale hasta su último día inclusive. El código pregunta «qué día es hoy» en la zona horaria de la tienda, Madrid:

src/main/java/shop/PromoCode.java
public boolean isValidToday() {
LocalDate today = LocalDate.now(ZoneId.of("Europe/Madrid"));
return !today.isAfter(lastDay);
}

Y la prueba crea un código cuyo último día es «hoy», según el reloj de la máquina:

src/test/java/shop/PromoCodeTest.java
@Test
@DisplayName("vale en su último día")
void validOnItsLastDay() {
PromoCode code = new PromoCode("OTONO10", LocalDate.now());
assertTrue(code.isValidToday());
}

En un portátil en Madrid los dos «hoy» son el mismo día, y la prueba siempre pasa. El servidor de CI está en la zona horaria UTC. Entre la medianoche y la 1:00 (las 2:00 en verano) hora de Madrid, en Madrid ya es mañana, pero en el servidor todavía es hoy. El código dice: el último día ya pasó. La prueba falla justo cuando se ejecuta la compilación nocturna. (En JavaScript la prueba toma siempre la fecha en UTC, así que a esa hora falla también en el portátil.)

A una prueba así se la llama inestable (flaky): a veces pasa y a veces falla, sin ningún cambio en el código. Las causas casi siempre son algo que la prueba no controla:

  • la hora y la fecha, la zona horaria;
  • el orden: la prueba depende de datos que dejó otra prueba;
  • las esperas: una pausa fija que a veces no basta;
  • la red y los servicios externos;
  • datos aleatorios sin una semilla fija.
Más precisamente, para curiosos

El código y la prueba de esta tarjeta se muestran y se compilan, pero no se ejecutan en el curso: su resultado depende del momento en que se ejecutan, y una comprobación con una salida esperada no es posible. Ese es justo su problema.

La idea

La solución: el día es una entrada

La prueba no puede controlar el reloj, así que el día se convierte en un parámetro. El código pregunta «¿vale en este día?»:

src/main/java/shop/PromoCode.java
// El código vale hasta su último día inclusive.
public boolean isValidOn(LocalDate day) {
return !day.isAfter(lastDay);
}

La prueba fija fechas concretas, no «hoy». Y ahora puede comprobar el límite con precisión: el último día y el día siguiente:

src/test/java/shop/PromoCodeTest.java
@Test
@DisplayName("vale en su último día")
void validOnItsLastDay() {
assertTrue(code.isValidOn(LocalDate.of(2026, 11, 30)));
}

El archivo entero, ejecutado. Pasa en cada ejecución, en cada máquina, a cada hora:

src/test/java/shop/PromoCodeTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PromoCodeTest {
private final PromoCode code = new PromoCode("OTONO10", LocalDate.of(2026, 11, 30));
@Test
@DisplayName("vale antes de su último día")
void validBeforeItsLastDay() {
assertTrue(code.isValidOn(LocalDate.of(2026, 11, 1)));
}
@Test
@DisplayName("vale en su último día")
void validOnItsLastDay() {
assertTrue(code.isValidOn(LocalDate.of(2026, 11, 30)));
}
@Test
@DisplayName("no vale al día siguiente")
void notValidTheDayAfter() {
assertFalse(code.isValidOn(LocalDate.of(2026, 12, 1)));
}
}
Salida
OK no vale al día siguiente
OK vale antes de su último día
OK vale en su último día
Pruebas: 3, pasan: 3, fallan: 0

Y si alguien escribe «antes del último día» en lugar de «hasta el último día inclusive», la prueba falla siempre, no a veces:

src/main/java/shop/PromoCode.java
// El código vale hasta su último día inclusive.
public boolean isValidOn(LocalDate day) {
return day.isBefore(lastDay);
}
src/test/java/shop/PromoCodeTest.java
package shop;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PromoCodeTest {
private final PromoCode code = new PromoCode("OTONO10", LocalDate.of(2026, 11, 30));
@Test
@DisplayName("vale antes de su último día")
void validBeforeItsLastDay() {
assertTrue(code.isValidOn(LocalDate.of(2026, 11, 1)));
}
@Test
@DisplayName("vale en su último día")
void validOnItsLastDay() {
assertTrue(code.isValidOn(LocalDate.of(2026, 11, 30)));
}
@Test
@DisplayName("no vale al día siguiente")
void notValidTheDayAfter() {
assertFalse(code.isValidOn(LocalDate.of(2026, 12, 1)));
}
}
Salida
FALLA vale en su último día: expected: <true> but was: <false>
OK no vale al día siguiente
OK vale antes de su último día
Pruebas: 3, pasan: 2, fallan: 1
Más precisamente, para curiosos

Java y C# tienen también una manera estándar de pasar el reloj desde fuera: java.time.Clock y TimeProvider (de .NET 8). Entonces el código pregunta al reloj, y la prueba le da uno fijo. En Python y JavaScript hay bibliotecas que congelan el tiempo en una prueba (freezegun, los temporizadores falsos de las herramientas de pruebas). La manera más sencilla, sin embargo, es la de esta tarjeta: el día es un parámetro.

La respuesta a «qué día es hoy para el código promocional» es una decisión del negocio, no del programador. Aquí se decidió: en la zona horaria de la tienda.

La idea

El informe

El informe de CI es lo primero que ve alguien cuando algo está en rojo. Un buen informe dice, sin preguntarle a nadie:

  • qué prueba falló, con un nombre que enuncia la regla («vale en su último día»), no test17;
  • esperado y real: «esperado: true, real: false»;
  • en qué cambio: quién subió qué;
  • el historial: si falla por primera vez, o también ayer y anteayer.

Las herramientas escriben el informe en un formato que CI entiende (con más frecuencia JUnit XML), y CI lo muestra junto al cambio.

Si una prueba falla a veces y arreglarla llevará tiempo, lo honesto es apartarla de las pruebas obligatorias (cuarentena), con un informe de defecto y un plazo, y hacer la comprobación a mano. No borrarla, y no ejecutarla hasta que pase.

Ejercicio

Rojo en CI

1. Causas y soluciones. Las preguntas son sobre el código promocional y el CI de la tienda.

1. La prueba «vale en su último día» falla solo en la compilación nocturna. ¿Qué pregunta haces primero?
2. ¿Cómo se arregla una prueba que depende de la fecha de hoy?
3. Una prueba falla a veces, y arreglarla llevará días. ¿Qué es lo honesto mientras tanto?
4. La prueba A pasa sola pero falla cuando la prueba B se ejecuta antes. ¿Cuál es la causa más probable?

Test

Ponte a prueba

1. ¿Qué es la integración continua (CI)?
2. ¿Por qué CI ejecuta las pruebas en una máquina limpia y no en el portátil del programador?
3. ¿Por qué es peligrosa una prueba que falla a veces?
4. ¿Qué debe decir el informe de una prueba que falló?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·