Inside-out TDD y outside-in TDD: arquitectura y diseño
Sabes hacer el ciclo de TDD (test en rojo, código mínimo, refactorizar), pero ante una funcionalidad nueva surge la duda: ¿por qué test empiezo? ¿Por la clase más pequeña o por el botón que pulsa el usuario? Esa decisión marca el diseño que saldrá, y por eso hablamos de arquitectura.
Piensa en un puzle. Puedes empezar por el centro: juntas grupos de piezas que encajan entre sí y, cuando los tienes, los unes (inside-out). O puedes empezar por el borde, lo que se ve desde fuera: sabes qué forma tiene el conjunto y vas rellenando hacia dentro, aunque todavía no sepas qué pieza irá en cada hueco (outside-in). El símil tiene un límite: en un puzle no puedes tapar un hueco con una pieza de mentira; en un test, sí, y eso son los mocks.
Las dos escuelas de un vistazo
Inside-out (clásica)
- Empieza por el dominio y sube hacia la interfaz
- Usa objetos reales siempre que puede
- Comprueba el estado final
- El diseño aparece al refactorizar
Outside-in (Londres)
- Empieza por el caso de uso y baja hacia el dominio
- Usa mocks para lo que aún no existe
- Comprueba los mensajes entre objetos
- El diseño aparece al escribir el test
Martin Fowler lo explica en «Mocks Aren’t Stubs». En el estilo clásico se usan objetos reales si es posible y un doble de prueba solo cuando el real es incómodo; en el mockista se usa un mock para cualquier objeto con un comportamiento interesante. Fowler añade que a veces se llama «Detroit» al estilo clásico y «Londres» al mockista. El nombre «Chicago» también es habitual para la escuela clásica, aunque Fowler no lo usa.
Si los términos mock, stub o spy te suenan pero no los distingues, empieza por la kata de stubs y spies.
Inside-out: de dentro hacia fuera
Empiezas por la regla de negocio más pequeña que puedes probar sola. Cuando funciona, la usas desde la capa de arriba, y así hasta llegar a la interfaz. Fowler describe una variante clásica que llama middle-out: decides qué objetos del dominio necesita la funcionalidad y añades la interfaz al final, a menudo sin ningún doble de prueba.
Ejemplo: registrar un usuario y dejarle un email de bienvenida. Primero, la pieza interna:
// usuario.mjs
export function crearUsuario(email) {
if (!email.includes('@')) throw new Error('Email no válido');
return { email: email.trim().toLowerCase() };
}
// usuario.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { crearUsuario } from './usuario.mjs';
// Paso 1: empezamos por la pieza más interna, el dominio.
test('normaliza el email', () => {
assert.equal(crearUsuario(' [email protected] ').email, '[email protected]');
});
test('rechaza un email sin arroba', () => {
assert.throws(() => crearUsuario('ana'), /no válido/);
});
Después subes una capa y usas esa pieza real. El almacén y el buzón son simples listas en memoria, y el test mira cómo quedan al final:
// registro.mjs
import { crearUsuario } from './usuario.mjs';
export function registrarUsuario(email, { usuarios, buzon }) {
const usuario = crearUsuario(email);
usuarios.push(usuario);
buzon.push({ para: usuario.email, asunto: 'Bienvenida' });
return usuario;
}
// registro.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { registrarUsuario } from './registro.mjs';
// Paso 2: subimos una capa con piezas reales y comprobamos el estado final.
test('guarda al usuario y le deja un email de bienvenida', () => {
const usuarios = [];
const buzon = [];
registrarUsuario('[email protected]', { usuarios, buzon });
assert.deepEqual(usuarios, [{ email: '[email protected]' }]);
assert.deepEqual(buzon, [{ para: '[email protected]', asunto: 'Bienvenida' }]);
});
Ventajas: los tests no dependen de cómo se llaman los objetos entre sí, así que puedes refactorizar por dentro sin romperlos. Riesgo: construir piezas que luego nadie usa, o descubrir tarde que el caso de uso necesitaba otra cosa.
Outside-in: de fuera hacia dentro
Empiezas por lo que pide el usuario. Steve Freeman y Nat Pryce lo desarrollan en «Growing Object-Oriented Software, Guided by Tests» (GOOS, 2009), el libro de referencia de este estilo según Fowler. Se trabaja en dos bucles:
- Escribes un test de aceptación de toda la funcionalidad. Fallará un buen rato.
- Bajas una capa y escribes un test unitario del objeto que atiende ese caso de uso.
- Sus colaboradores todavía no existen: los sustituyes por mocks y decides qué mensajes deben recibir.
- Implementas cada colaborador con el mismo ciclo, capa a capa, hasta que el test de aceptación pasa.
El mismo registro, empezando por fuera:
// registro.test.mjs
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { registrarUsuario } from './registro.mjs';
// El repositorio y el notificador aún no existen: los sustituimos
// por mocks y el test decide qué mensajes deben recibir.
test('guarda al usuario y pide que le den la bienvenida', () => {
const repositorio = { guardar: mock.fn() };
const notificador = { darBienvenida: mock.fn() };
registrarUsuario('[email protected]', { repositorio, notificador });
const usuario = { email: '[email protected]' };
assert.deepEqual(repositorio.guardar.mock.calls[0].arguments, [usuario]);
assert.deepEqual(notificador.darBienvenida.mock.calls[0].arguments, [usuario]);
});
// registro.mjs
export function registrarUsuario(email, { repositorio, notificador }) {
const usuario = { email };
repositorio.guardar(usuario);
notificador.darBienvenida(usuario);
return usuario;
}
Fíjate en lo que ha pasado: antes de escribir una línea del repositorio o del notificador, el test ya ha decidido que existen y qué métodos tienen. Fowler cuenta que los mockistas defienden un estilo llamado need-driven development: cada capa descubre lo que necesita de la siguiente.
Ventajas: solo construyes lo que el caso de uso pide y las interfaces nacen de su uso real. Riesgo: los tests saben cómo hablan los objetos entre sí. Si renombras guardar, el test se rompe aunque el comportamiento sea el mismo. Y un test lleno de mocks puede pasar aunque las piezas reales no encajen; para eso está el test de aceptación del bucle exterior.
Inside-out pregunta qué sé hacer. Outside-in pregunta qué me piden.
Qué tiene que ver con la arquitectura
Mucho, porque cada escuela empuja hacia una forma de diseño:
- Outside-in encaja con la arquitectura hexagonal (puertos y adaptadores). Los mocks del ejemplo,
repositorioynotificador, son justo los puertos: contratos que el caso de uso define y que luego implementan una base de datos o un servicio de correo. Esta relación es una interpretación de este artículo, no de los autores: GOOS habla de objetos que se envían mensajes y de escuchar a los tests, no de hexágonos. - Inside-out favorece un dominio rico y probado sin dobles, que es el corazón de cualquier arquitectura limpia. Su riesgo es que la frontera con el exterior se diseñe tarde y a toda prisa.
No son bandos. El propio Kent Beck escribió en el prólogo de GOOS: «The style of test-driven development presented here is different from what I practice» («el estilo de TDD que se presenta aquí es distinto del que practico»), y aun así recomienda el libro. Una forma habitual de combinarlos: outside-in para las fronteras y la orquestación del caso de uso, inside-out para la lógica de negocio pura.
| Si… | Prueba con |
|---|---|
| La lógica de negocio es compleja (cálculos, reglas) | Inside-out |
| La funcionalidad coordina varios servicios o capas | Outside-in |
| Aún no sabes qué necesita el usuario | Outside-in, con un test de aceptación primero |
| Quieres tests que aguanten refactorizaciones grandes | Inside-out, o mocks solo en las fronteras |
El test de aceptación del bucle exterior es la misma idea que el ATDD: lo tienes explicado en desarrollo dirigido por tests de aceptación.
En una aplicación con el patrón modelo vista controlador, outside-in suele empezar por el controlador, que es la puerta de entrada de cada petición, e inside-out por el modelo.
Y cuando programas con IA
Como criterio del enfoque IA Limpia, las dos escuelas sirven para poner límites a un agente:
- Outside-in: escribe tú el test de aceptación antes de pedir nada. Es el contrato que el agente no puede tocar y la prueba de que el resultado hace lo que pediste.
- Inside-out: pide piezas pequeñas, cada una con su test, y revísalas antes de subir de capa. Es más fácil revisar una función de diez líneas que un caso de uso entero.
- Vigila los mocks: un agente puede generar tests que solo comprueban que el código se llama a sí mismo como ya lo hace. Si un test no fallaría ante un error real, no protege nada.
El vídeo original
Este artículo nació para acompañar el vídeo «Introducción a outside-in TDD con JavaScript», del canal de YouTube «Carlitos y sus cosas» (@carlosble), de Carlos Blé.
| Qué es | La grabación de una clase con la Generación K, del 15 de junio de 2018 |
| De qué trata | Según su descripción: inside-out TDD, outside-in TDD, arquitectura y diseño de software |
| Duración | 1 h 19 min, sin capítulos |
| Idioma | Español |
| Precio | Gratis en YouTube |
Es una clase grabada, no un vídeo editado: conviene verla con tiempo y, si puedes, con el editor abierto para repetir los ejemplos en JavaScript. Tiene unos años, así que la sintaxis o las herramientas que aparezcan pueden haber cambiado; las ideas de las dos escuelas siguen igual. Los ejemplos de este artículo, con node:test de Node 24, sirven como versión actualizada para practicar.
Siguiente paso
Elige una funcionalidad pequeña y hazla dos veces: una empezando por el dominio y otra empezando por el caso de uso. Después compara qué tests se rompen al cambiar un nombre de método. Esa diferencia es la que te ayudará a elegir en tu próximo proyecto.
Fuentes
- Martin Fowler, «Mocks Aren’t Stubs» (revisado el 2 de enero de 2007): definición de los estilos clásico y mockista, los nombres «Detroit» y «Londres», need-driven development, outside-in y middle-out, y la recomendación de Freeman y Pryce. Consultado en noviembre de 2026.
- Steve Freeman y Nat Pryce, Growing Object-Oriented Software, Guided by Tests, Addison-Wesley, primera impresión de octubre de 2009. La cita de Kent Beck procede del prólogo, leído en la muestra oficial de Pearson.
- El nombre «escuela de Chicago» es de uso común en la comunidad, pero no se ha localizado en una fuente primaria; por eso no se atribuye a nadie.
- Documentación de
node:test(Node.js 24.x):mock.fn()ymock.calls[i].arguments. - Datos del vídeo (título, canal, fecha, duración y descripción) comprobados en su página de YouTube y en su oEmbed el 7 de noviembre de 2026. El contenido del vídeo no se ha transcrito: este artículo no atribuye a Carlos Blé nada de lo que se explica arriba.
- Los dos ejemplos se han ejecutado con
node --testen Node.js 24.21.0: los tres tests de inside-out y el de outside-in pasan.