Principios Fundamentales: F.I.R.S.T.
Fast, Independent, Repeatable, Self-Validating, Timely — la checklist mnemotécnica para unit tests, y por qué Fast y Repeatable son los que más problemas causan en la práctica.
Una regla mnemotécnica muy usada para pensar unit tests es F.I.R.S.T., atribuida a Tim Ottinger y Jeff Langr, y popularizada por Clean Code de Robert C. Martin. No es un estándar formal ni una especificación: funciona como checklist de buenas prácticas.
| Principio | Qué significa | Riesgo si se ignora |
|---|---|---|
| Fast | Corre en milisegundos, sin arrancar servicios reales | Fricción para correr la suite seguido → se corre menos |
| Independent | No depende de otros tests ni de shared state | Failures en cascada sin relación con el bug real |
| Repeatable | Mismo resultado siempre, bajo las mismas condiciones | Flaky tests que erosionan la confianza en la suite |
| Self-Validating | Determina por sí mismo si pasó o falló, sin revisión manual | No se puede correr en CI/CD sin intervención humana |
| Timely | Se escribe cerca del momento en que se implementa el comportamiento | El test documenta código viejo en vez de guiar el nuevo |
Vale la pena detenerse en dos de estos principios, porque son los que más problemas causan en la práctica.
Fast
No tiene sentido que un test de una business rule dependa de levantar la aplicación, la base de datos y un request HTTP cuando el comportamiento se puede verificar sin ninguna de esas dependencias. La velocidad no es un objetivo aislado: habilita un ciclo de feedback corto, y permite asociar un failure con los cambios más recientes.
Repeatable
Un test que falla de forma intermitente —un flaky test— es especialmente costoso, porque hace difícil distinguir un defecto real de un problema de timing o de infraestructura. Las causas más comunes son la hora actual, randomness, shared state, race conditions y dependencias de red.
Por ejemplo, si calculateCancellationFee tomara la hora de la reserva y calculara internamente cuántas horas faltan con new Date(), el resultado del test dependería del momento exacto en que se ejecuta. La solución habitual es inyectar una abstracción controlable:
function calculateCancellationFee(
deposit: Money,
reservationTime: Date,
clock: Clock = systemClock
): Money {
const hoursBeforeReservation = clock.hoursUntil(reservationTime);
if (hoursBeforeReservation >= 24) return Money.zero();
return deposit.multiply(0.5);
}
Así el test puede definir explícitamente el instante utilizado, en lugar de depender de cuándo se corre la suite. El mismo principio se aplica a la generación de IDs, los valores random, las variables de entorno y cualquier otro input no determinista.
Fijate que esta versión resuelve dos cosas: calcula las horas restantes y decide la penalidad. La alternativa —dejar calculateCancellationFee(deposit, hoursBeforeReservation) como una función pura y calcular las horas afuera— es determinista sin necesidad de inyectar nada. La forma más barata de tener un test repetible suele ser no dejar entrar el no-determinismo en la unidad, antes que controlarlo con una abstracción. Los artículos que siguen usan esa versión pura.
Con la unidad bajo test aislada y determinista, el siguiente problema es cómo estructurar el test en sí — de eso trata el patrón AAA.
