Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Una prueba unitaria es código que ejerce otro código y afirma —mediante una aserción— que el resultado observado coincide con el esperado. Suena modesto, pero es la pieza que sostiene todo lo demás: sin una red de pruebas que se ejecuta en segundos, cada cambio es una apuesta y el miedo a tocar el código se convierte en deuda. Kent Beck, en Test-Driven Development: By Example, invierte el orden habitual: primero se escribe la prueba que falla (rojo), después el código mínimo que la hace pasar (verde) y por último se refactoriza con la prueba como red de seguridad. Ese ciclo red-green-refactor no es una ceremonia; es una forma de dejar que el diseño emerja de ejemplos concretos en lugar de especulación.
El problema de juguete de esta clase —comprobar que a + b == esperado— es deliberadamente trivial para que el foco esté en el mecanismo de la aserción, no en la lógica bajo prueba. Steve McConnell, en Code Complete, insiste en que la calidad no se "añade" al final: se construye con prácticas que hacen visible cada defecto lo antes posible, y la prueba automática es la más barata de todas. Aquí verás que el mismo acto —afirmar una igualdad y declararla pasa o falla— existe en los diez lenguajes del núcleo, cada uno con su runner: pytest, JUnit, Jest/vitest, cargo test, go test, dotnet test o PHPUnit. Comprender ese denominador común te permite moverte entre ecosistemas sin reaprender la idea, solo la herramienta.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Prueba unitaria | Es el bloque mínimo de la red de seguridad; corre en milisegundos y aísla el fallo |
| 2 | Aserción | Convierte una expectativa implícita en una comprobación explícita y automatizable |
| 3 | Pasa/falla (verde/rojo) | El veredicto binario guía el ciclo TDD y el semáforo de la CI |
| 4 | Runner por lenguaje | Cada ecosistema descubre y ejecuta las pruebas con su propia herramienta |
test={'pasa' if a + b == esperado else 'falla'} es una aserción desnuda, sin framework: expone la mecánica que herramientas como assert (Python), assertEquals (JUnit) o expect().toBe() (Jest) envuelven con mejores mensajes de error.pytest, cargo test, go test ./...— recorre todo el conjunto. Su valor está en el reporte: qué falló, con qué valor esperado frente al obtenido, y en qué línea.Trabajas en una biblioteca de facturación y alguien "optimiza" la función de sumatoria de un pedido. Compila, se despliega, y tres días después contabilidad reporta totales de un céntimo de menos por redondeo. Con una sola prueba unitaria —sumar(3, 4) debe dar 7, y un caso límite con decimales— ese error se habría iluminado en rojo en el instante del cambio, antes de salir del portátil. Esa es la promesa de McConnell: mover el descubrimiento del defecto lo más cerca posible de su introducción, donde arreglarlo cuesta minutos y no incidentes.
a b esperadotest=<pasa|falla>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
3 4 7 |
test=pasa |
2 2 5 |
test=falla |
10 5 15 |
test=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/. Cada programa es a la vez el sujeto y la aserción: lee a b esperado, calcula la suma y emite el veredicto. En un proyecto real separarías la función bajo prueba del código que la prueba, pero aquí los fundimos para que veas la comparación esencial sin el andamiaje del framework.
python/main.py · python main.pyimport sys
a, b, esperado = map(int, sys.stdin.readline().split())
print(f"test={'pasa' if a + b == esperado else 'falla'}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
Léelo despacio, porque condensa toda la clase en dos líneas. sys.stdin.readline().split() parte la línea 3 4 7 en tres cadenas; map(int, …) las convierte a enteros y el desempaquetado los ata a a, b y esperado. La segunda línea es la aserción: la expresión condicional 'pasa' if a + b == esperado else 'falla' evalúa exactamente el mismo predicado que escribirías dentro de un assert a + b == esperado de pytest. La diferencia es que pytest, al fallar, te mostraría el valor real y el esperado con su introspección de aserciones; aquí lo reducimos a la palabra pasa o falla para que el verificador del curso pueda compararla carácter a carácter. Con la entrada 2 2 5 la suma da 4, no 5, y la salida es test=falla: una prueba en rojo, tal como Beck quiere verla antes de escribir el arreglo.
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const [a, b, esperado] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);
console.log(`test=${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(`test=${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]), esperado = Integer.parseInt(p[2]);
System.out.println("test=" + (a + b == esperado ? "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($"test={(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])
esperado, _ := strconv.Atoi(f[2])
res := "falla"
if a+b == esperado {
res = "pasa"
}
fmt.Printf("test=%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!("test={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, esperado;
if (scanf("%ld %ld %ld", &a, &b, &esperado) != 3) return 1;
printf("test=%s\n", a + b == esperado ? "pasa" : "falla");
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: una consulta de comprobación.
WITH t(a, b, esperado) AS (VALUES (3, 4, 7))
SELECT printf('test=%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, $esperado] = array_map('intval', preg_split('/\s+/', trim(fgets(STDIN))));
echo "test=" . ($a + $b === $esperado ? "pasa" : "falla") . "\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
Compara ahora tres contrastes reveladores. En Rust, v[0] + v[1] == v[2] opera sobre i64 con desbordamiento comprobado en modo debug: si la suma se saliera del rango, el programa entraría en pánico en lugar de dar un resultado silenciosamente erróneo —una forma de aserción que el propio lenguaje impone, en la línea de lo que Klabnik y Nichols describen sobre la seguridad de Rust. En C, scanf("%ld %ld %ld", …) puede fallar si la entrada no trae tres números, y por eso el if (… != 3) return 1; es su propia mini-aserción de contrato: sin frameworks, la disciplina de comprobar el valor de retorno es lo que separa un programa robusto de uno que corrompe memoria, tal como enseñan Kernighan y Ritchie. En SQL, en cambio, no hay stdin ni bucle: la comprobación es una CASE WHEN a + b = esperado sobre una tabla de casos, la forma declarativa de expresar la misma verdad. El veredicto textual —test=pasa— es idéntico en los tres; lo que cambia es cómo cada lenguaje llega a él.
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.
La sintaxis de la aserción es lo de menos; lo que de verdad distingue a cada ecosistema es su runner, cómo descubre las pruebas y cómo las ejecuta.
| Lenguaje | Runner habitual | Cómo se invoca | Aserción idiomática |
|---|---|---|---|
| Python | pytest / unittest | pytest |
assert a + b == esperado |
| JavaScript | Jest / vitest | npx jest · npx vitest |
expect(x).toBe(y) |
| TypeScript | vitest / Jest | npx vitest |
expect(x).toBe(y) |
| Java | JUnit 5 | mvn test · gradle test |
assertEquals(y, x) |
| C# | xUnit / NUnit | dotnet test |
Assert.Equal(y, x) |
| Go | testing (estándar) | go test ./... |
if got != want { t.Errorf(...) } |
| Rust | test (integrado) | cargo test |
assert_eq!(x, y) |
| C | Unity / Criterion | según build | TEST_ASSERT_EQUAL(y, x) |
| SQL | pgTAP / assert manual | según motor | CASE WHEN … THEN 'ok' |
| PHP | PHPUnit | ./vendor/bin/phpunit |
$this->assertSame(y, x) |
Nota los dos extremos culturales. Go y Rust traen el runner dentro del propio lenguaje: no instalas nada, go test y cargo test descubren las funciones de prueba por convención (func TestXxx y #[test]) y las corren. En el otro lado, Java, C# o PHP delegan en herramientas externas (JUnit, xUnit, PHPUnit) que se integran con el gestor de dependencias. Esa decisión de diseño —¿pruebas de primera clase en el lenguaje o biblioteca aparte?— condiciona cuánta fricción hay para empezar a probar, y explica por qué en Go probar es el camino de menor resistencia.
pytest, JUnit, xUnit, cargo test, go test o PHPUnit son dialectos de la misma idea que formalizó la familia xUnit de Kent Beck: un test case que prepara (arrange), ejerce (act) y afirma (assert). Cambia el nombre del método de aserción y la forma de descubrir las pruebas, pero el ciclo rojo-verde-refactor es idéntico en todas partes; por eso quien lo interioriza en un lenguaje lo transporta a los demás casi sin coste.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 139
Detalle en reto.md.
assert x sin contexto te dice que algo falló, no qué. Los buenos frameworks muestran esperado-vs-obtenido; aprovéchalo y evita la aserción muda.casos.json es una prueba? Sí, es una prueba de caja negra: fija una entrada y la salida esperada, y el verificador compara la real con la esperada. Es equivalente a una tabla de casos parametrizados de pytest o JUnit.Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 138 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 140 ⏭️