Parte 11 — Proyecto integrador políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Ejercitar el sistema completo, de la entrada a la salida, como lo haría un usuario real: eso es una prueba end-to-end. Hasta ahora cada clase de esta parte construyó una pieza y confió en que su contrato se cumpliera. Hoy comprobamos lo único que ninguna prueba de componente puede comprobar: que las piezas, juntas, producen el resultado que el usuario espera. La operación es deliberadamente mínima —dadas dos entradas y un valor esperado, decir si el sistema acierta—, porque el fondo de una prueba e2e es siempre ese: comparar lo observado con lo prometido.
La razón de que exista esta categoría de prueba es que los sistemas fallan en las costuras. Cada componente puede pasar sus pruebas unitarias al 100 % y el sistema seguir roto, porque la API devuelve céntimos y la web asume euros, o porque el script nocturno escribe una fecha en un formato que la base de datos interpreta al revés. Sam Newman lo formula sin rodeos en Building Microservices: cuanto más se descompone un sistema, más se desplaza el riesgo desde el interior de las piezas hacia el espacio entre ellas. La prueba e2e es la única que mira ese espacio entero de una vez — y también, por eso mismo, la más lenta, la más frágil y la que más caro sale mantener.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | End-to-end | El sistema completo |
| 2 | Flujo de usuario | De la entrada a la salida |
| 3 | Pirámide de pruebas | Muchas unitarias, pocas e2e |
Una prueba end-to-end verifica el sistema completo desde la perspectiva de quien lo usa: entra por la misma puerta que el usuario, atraviesa todos los componentes reales —sin sustituir ninguno por un doble— y comprueba la salida final. Un flujo es el recorrido concreto de una acción a través de ese sistema ("registrarse", "hacer un pedido", "cerrar el mes"); es la unidad que una prueba e2e ejercita, y por eso sus casos se escriben en el lenguaje del negocio y no en el de las funciones. La pirámide de pruebas es la forma que debe tener la suite completa: una base ancha de pruebas unitarias rápidas, una franja media de integración y una punta estrecha de e2e.
Conviene entender por qué la pirámide tiene esa forma y no otra, porque no es una convención estética. Cada escalón hacia arriba multiplica dos cosas a la vez: la confianza que da un test que pasa y el coste de tenerlo. Una prueba unitaria corre en milisegundos, señala con precisión la función culpable y casi nunca falla por motivos ajenos; pero no puede detectar un desajuste de contrato entre dos servicios. Una e2e sí lo detecta, y a cambio tarda minutos, depende de red, datos y relojes, y cuando falla te dice "algo del sistema está mal" sin decirte qué. Ese último punto es el decisivo: una prueba que falla de forma intermitente —lo que se llama un test flaky— no solo no ayuda, sino que destruye la utilidad de toda la suite, porque el equipo aprende a ignorar los fallos en rojo. Hunt y Thomas insisten en The Pragmatic Programmer en que una prueba vale por la decisión que te permite tomar sin pensarlo dos veces; una suite en la que nadie confía no permite tomar ninguna.
Tu equipo despliega y en producción los totales salen mal por un céntimo. Las pruebas unitarias del backend pasan: la función de cálculo es correcta. Las del frontend pasan: formatea bien lo que recibe. El error está en el medio —el backend devuelve un decimal en coma flotante y el frontend redondea antes de sumar el IVA—, y ninguna prueba de componente podía verlo, porque cada una probaba su lado del contrato asumiendo que el otro se comportaba como esperaba. Una sola prueba e2e que introduzca un pedido real y compare el total final con el esperado detecta ese fallo en el primer intento. Ese es exactamente el hueco que llena esta categoría: no comprueba que las piezas sean correctas, comprueba que encajan.
a b esperadoe2e=<pasa|falla>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
3 4 7 |
e2e=pasa |
2 2 5 |
e2e=falla |
10 5 15 |
e2e=pasa |
LEER a, b, esperado ; SI a+b == esperado: pasa SINO falla
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyimport sys
a, b, esperado = map(int, sys.stdin.readline().split())
print(f"e2e={'pasa' if a + b == esperado else 'falla'}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const [a, b, esperado] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`e2e=${a + b === esperado ? "pasa" : "falla"}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const [a, b, esperado] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`e2e=${a + b === esperado ? "pasa" : "falla"}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] p = br.readLine().trim().split("\\s+");
int a = Integer.parseInt(p[0]), b = Integer.parseInt(p[1]), e = Integer.parseInt(p[2]);
System.out.println("e2e=" + (a + b == e ? "pasa" : "falla"));
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
int[] p = Array.ConvertAll(Console.In.ReadToEnd()
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries), int.Parse);
Console.WriteLine($"e2e={(p[0] + p[1] == p[2] ? "pasa" : "falla")}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
f := strings.Fields(line)
a, _ := strconv.Atoi(f[0])
b, _ := strconv.Atoi(f[1])
e, _ := strconv.Atoi(f[2])
res := "falla"
if a+b == e {
res = "pasa"
}
fmt.Printf("e2e=%s\n", res)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainuse std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let v: Vec<i64> = s.split_whitespace().map(|x| x.parse().unwrap()).collect();
let res = if v[0] + v[1] == v[2] { "pasa" } else { "falla" };
println!("e2e={res}");
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
int main(void) {
long a, b, e;
if (scanf("%ld %ld %ld", &a, &b, &e) != 3) return 1;
printf("e2e=%s\n", a + b == e ? "pasa" : "falla");
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL prueba con una consulta de comprobacion.
WITH t(a, b, esperado) AS (VALUES (3, 4, 7))
SELECT printf('e2e=%s', CASE WHEN a + b = esperado THEN 'pasa' ELSE 'falla' END) AS resultado FROM t;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
[$a, $b, $e] = array_map('intval', preg_split('/\s+/', trim(fgets(STDIN))));
echo "e2e=" . ($a + $b === $e ? "pasa" : "falla") . "\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
El contrato (casos.json) recibe a b esperado y responde e2e=pasa o e2e=falla: el
sistema bajo prueba es la suma a + b, y el tercer número es la promesa contra la que se contrasta. Esa
estructura —ejecutar, observar, comparar con lo esperado— es la de cualquier aserción, desde un
assert de una línea hasta una suite de Playwright que pilota un navegador. Fíjate en que el caso
2 2 5 produce e2e=falla y aun así el programa termina con éxito: distinguir "el test falló" de "el
ejecutor de tests se rompió" es una de las diferencias que un arnés de pruebas real debe respetar.
Los diez lenguajes convergen en la misma línea de decisión, pero la escriben con formas que revelan su familia. Python, JavaScript, TypeScript, Java, C#, Rust y PHP usan una expresión condicional que devuelve un valor:
print(f"e2e={'pasa' if a + b == esperado else 'falla'}")
La comparación y la elección del texto ocurren dentro de la misma expresión. Go no tiene operador ternario —una omisión deliberada de sus diseñadores, que Donovan y Kernighan explican como preferencia por un único camino legible— y obliga a una sentencia con una variable previa:
res := "falla"
if a+b == e {
res = "pasa"
}
Inicializar en "falla" y ascender a "pasa" solo si la condición se cumple es, además, el orden correcto
para un test: el veredicto por defecto es el negativo, y el éxito hay que ganárselo. C aprovecha su
ternario para pasarlo directamente a printf, y SQL usa CASE WHEN … THEN … ELSE … END, que es la
misma idea en forma de expresión declarativa aplicada a una tabla de casos: exactamente como se escriben
las pruebas basadas en datos, donde los casos son filas y no líneas de código.
Comparar un resultado con un valor esperado es trivial; lo interesante es qué forma le da cada familia a esa decisión y qué dice esa forma sobre el lenguaje.
| Clase de diferencia | Observación entre lenguajes |
|---|---|
| Sintáctica | Ternario ?: (JS/TS/Java/C#/C/PHP), expresión condicional (Python), if como expresión (Rust), if como sentencia (Go), CASE WHEN (SQL). |
| Semántica | En Python, JS y PHP la comparación puede convertir tipos si no se cuida (=== frente a ==); en Java, C#, Go y Rust el compilador impide comparar tipos incompatibles antes de ejecutar nada. |
| Paradigmática | Los imperativos ejecutan el flujo y luego comprueban; SQL expresa la comprobación sobre un conjunto de casos — una fila por caso de prueba, que es el modelo de las pruebas parametrizadas. |
Hay una lección de fondo que este ejercicio hace visible sin decirlo: la prueba está escrita en el mismo
lenguaje que el componente. En un sistema políglota real eso deja de ser posible, porque ningún lenguaje
puede probar el flujo completo desde dentro. Por eso las pruebas e2e viven fuera de los componentes —en
un script, un contenedor o una herramienta dedicada— y hablan con el sistema por sus fronteras públicas:
HTTP, stdin/stdout, la interfaz gráfica. El verificador de este propio programa, que corre las diez
implementaciones contra casos.json y compara salidas, es precisamente eso: un arnés e2e políglota.
Toda familia de lenguajes tiene su capa de pruebas —pytest y unittest en Python, JUnit en la JVM, xUnit
y NUnit en .NET, go test integrado en el propio toolchain de Go, cargo test en Rust, Jest y Vitest en
JavaScript, PHPUnit en PHP— y todas implementan la misma tríada: preparar, ejecutar, afirmar. Para el
escalón e2e el reparto es distinto, porque la herramienta ya no depende del lenguaje del componente sino
del canal por el que se entra al sistema: Playwright, Cypress y Selenium pilotan un navegador real;
curl, Postman o k6 golpean una API por HTTP; un simple script de shell ejercita una CLI. Reconocer que
todas son la misma idea aplicada a puertas distintas es lo que te permite montar una suite e2e en un
sistema cuyos lenguajes no dominas todavía.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 173
Detalle en reto.md.
sleep 2 funciona en tu portátil y falla en CI, que va más lento → solución: esperar a que ocurra el suceso (el elemento aparece, la respuesta llega) con un límite de tiempo, nunca una duración a ojo.Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 172 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 174 ⏭️