Me encanta el testing. La idea de tener código que ejecuta otras piezas de código para verificar que lo que hace es correcto me encaja por todos lados. Y bastante literalmente, porque deberías ver un montón de ✅ (¡ojalá!) al ejecutar los tests. Sin embargo, testear de la forma correcta es bastante complejo y en este artículo te voy a enseñar a testear de forma efectiva desde más de una década de experiencia.
expect(1 + 1).toBe(2);#Decidir cómo testear
Existe una gran variedad de tipos de tests:
- Unitario: Testea una pieza de lógica de forma aislada, rápido y enfocado.
- Integración: Testea que varias piezas funcionan bien juntas, como tu código contra una base de datos real.
- E2E: Testea el sistema completo desde fuera, como lo haría un usuario.
- Caracterización: Captura lo que el código legacy hace actualmente, para que puedas refactorizar sin cambiar el comportamiento.
- Rendimiento: Mide velocidad y carga contra un umbral definido.
- Basado en propiedades: Declara una regla que siempre debe cumplirse y genera cientos de entradas para intentar romperla.
- Mutación: Introduce pequeños bugs en tu código para comprobar si tus tests los detectan.
- Regresión: Fija un bug ya corregido para que no pueda volver jamás.
En este artículo nos vamos a centrar en los principios que de verdad hacen robusto el testing en todos los tipos de tests. Sin embargo, antes de escribir una sola línea de código (o de pedírselo a tu agente de IA), primero tenemos que pensar qué es lo que más nos beneficia en este momento.
Por ejemplo, el testing E2E es genial para empezar si no tienes tests previos, ya que así puedes verificar que la funcionalidad principal funciona como debería (¡eso es lo que se conoce como happy paths!). El reto en proyectos sin tests es que, aunque testear en sí es bastante sencillo, lo difícil es tener código que sea fácil de testear.
“”Testear es fácil, lo difícil es tener código testeable
Empezar con tests E2E esquiva ese reto y a la vez aporta valor real al proyecto, porque testea el artefacto resultante en lugar de los internos.
Después, el paso natural es avanzar hacia los tests unitarios y luego los de integración. A veces, por el camino, podemos añadir tests de caracterización para validar el comportamiento actual de algo.
Para proyectos sin testing, este es el orden que sigo:
- E2E
- Caracterización
- Unitarios
- Integración
- Mutación
- Basados en propiedades
- Regresión
- Rendimiento
Si el proyecto ya tiene testing, entonces toca pensar dónde estamos y qué aporta valor al proyecto, y me temo que esa pregunta solo la puedes responder tú.
#Decidir qué testear
Una vez que hemos decidido cómo testear nuestra aplicación, toca decidir qué testear. ¿Deberíamos aspirar al 100% de cobertura? ¿Testear solo la funcionalidad esencial? ¿Solo los ficheros con un número impar de letras?
La cobertura es el porcentaje de tu código que se ejecuta cuando corren tus tests. Ejecutado no significa verificado:
// Código de producción
function applyDiscount(price: number, percent: number): number {
return price - (price * percent) / 100
}
// Test con 100% de cobertura... que no verifica nada
test('applies discount', () => {
applyDiscount(100, 20) // se ejecuta, nunca se comprueba
})
// Test con la misma cobertura, pero con verificación real
test('applies discount', () => {
expect(applyDiscount(100, 20)).toBe(80)
})Yo me inclino por testear lo que tiene sentido. Respuesta decepcionante, ¿verdad? Sin embargo, creo que es la respuesta correcta. He estado en proyectos cerca del 100% de cobertura y aun así no me fiaba del código. Los tests testeaban código interno en lugar de la funcionalidad, o estaban duplicados.
Adquirir el criterio de qué testear lleva tiempo.
Aquí tienes un ejemplo de un gran test y de un test pobre:
// ✅ Un gran test: verifica el comportamiento a través de la API pública
describe('Cart', () => {
it('applies a 10% discount to the total when a valid coupon is used', () => {
const cart = new Cart();
cart.add({ name: 'Keyboard', price: 100 });
cart.add({ name: 'Mouse', price: 50 });
cart.applyCoupon('SAVE10');
expect(cart.total()).toBe(135);
});
});// ❌ Un test pobre: acoplado a los internos, no verifica nada que le importe al usuario
describe('Cart', () => {
it('works', () => {
const cart = new Cart();
const spy = vi.spyOn(cart as any, 'recalculate');
cart.add({ name: 'Keyboard', price: 100 });
expect(spy).toHaveBeenCalledTimes(1);
expect((cart as any).items.length).toBe(1);
expect((cart as any).discount).toBe(0);
});
});La diferencia se hace enorme a medida que el proyecto crece.
Así que céntrate siempre en testear la API pública y en cómo un usuario usaría realmente el código.
#AAA
En los tests anteriores puede que hayas visto un salto de línea en sitios aparentemente arbitrarios.
describe('Cart', () => {
it('applies a 10% discount to the total when a valid coupon is used', () => {
const cart = new Cart();
cart.add({ name: 'Keyboard', price: 100 });
cart.add({ name: 'Mouse', price: 50 });
// Aquí
cart.applyCoupon('SAVE10');
// Aquí
expect(cart.total()).toBe(135);
});
});Pues no, no es arbitrario.
Lo hago para agrupar bajo el patrón AAA, que significa Arrange, Act y Assert (preparar, actuar y verificar).
- Arrange: Aquí es donde preparamos los mocks, creamos las instancias y todos los datos necesarios para los tests
- Act: Ejecutamos el código que realmente necesita ejecutarse para testear lo que queremos (también conocido como Subject Under Test o SUT)
- Assert: Hacemos las verificaciones necesarias. Yo tiendo a tener una verificación por test
¿Repites los mismos mocks una y otra vez? Lee sobre el patrón de diseño mother.
Si el bloque de arrange se duplica entre tests, creo una función de setup y la muevo al final del fichero. Uso un objeto para configurar el setup si hace falta. Prefiero este enfoque antes que el beforeEach porque resulta en menos código:
function setup() {
const cart = new Cart();
cart.add({ name: 'Keyboard', price: 100 });
cart.add({ name: 'Mouse', price: 50 });
return cart;
}Contra
describe('Cart', () => {
let cart: Cart;
beforeEach(() => {
cart = new Cart();
cart.add({ name: 'Keyboard', price: 100 });
cart.add({ name: 'Mouse', price: 50 });
});
it('applies a 10% discount to the total when a valid coupon is used', () => {
cart.applyCoupon('SAVE10');
expect(cart.total()).toBe(135);
});
});La primera opción es más versátil y configurable, porque podemos pasarle nuevas opciones por parámetros. Además favorecemos el código inmutable:
function setup({ items = [{ name: 'Keyboard', price: 100 }, { name: 'Mouse', price: 50 }] } = {}) {
const cart = new Cart();
items.forEach(item => cart.add(item));
return cart;
}#TDD
Si combinas todo lo anterior con Test Driven Development obtienes, en mi opinión, la forma más fiable de construir software. TDD es engañosamente simple. Repites tres pasos, en este orden exacto:
El ciclo de TDD
Escribe un test que falle y que describa el comportamiento que quieres.
Vamos a verlo todo en acción con la clásica kata FizzBuzz.
El problema: escribe una función que recibe un número y devuelve:
- "fizz" si el número es divisible por 3
- "buzz" si el número es divisible por 5
- "fizzbuzz" si es divisible por ambos
- El propio número, como string, en cualquier otro caso
Empecemos creando el primer test:
it('returns fizz when divisible by 3', () => {
expect(fizzbuzz(3)).toBe('fizz');
});function fizzbuzz(number: number): string {
return 'fizz';
}Sí, en serio. El test pasa, y eso es lo único que importa ahora mismo. Parece absurdo, pero obliga a que los tests dirijan la implementación, y no nuestras suposiciones. Cada nuevo test aporta nuevas suposiciones:
it('returns the number as a string when not divisible by 3 or 5', () => {
expect(fizzbuzz(1)).toBe('1');
});function fizzbuzz(number: number): string {
if (number % 3 === 0) return 'fizz';
return String(number);
}Lo mismo para buzz, hasta llegar al test final:
it('returns fizzbuzz when divisible by both 3 and 5', () => {
expect(fizzbuzz(15)).toBe('fizzbuzz');
});¡Rojo! fizzbuzz(15) devuelve 'fizz' porque el primer if gana. Los tests acaban de cazar un bug real en nuestro diseño:
function fizzbuzz(number: number): string {
if (number % 15 === 0) return 'fizzbuzz';
if (number % 3 === 0) return 'fizz';
if (number % 5 === 0) return 'buzz';
return String(number);
}Todo en verde. Ahora, y solo ahora, refactorizamos con total confianza, porque cualquier paso en falso pone un test en rojo al instante.
Te recomiendo hacer un commit después de pasar a verde
Cada implementación obligó al siguiente test a ser más específico, y cada test obligó al código a ser más general. Eso es TDD.
#Testing en la era de la IA
Entonces, ¿por qué el título de este artículo? Porque todo lo anterior acaba de volverse más importante, no menos.
Los agentes pueden generar más código en una tarde del que antes escribíamos en una semana. El código ahora es barato. La confianza es, sin embargo, el problema reto. Y una buena suite de tests es la forma más eficiente de conseguir confianza.
Cuando un agente escribe código, los tests se convierten en el contrato: el agente los ejecuta, lee los fallos e itera hasta que todo está en verde. Por eso TDD y los agentes encajan tan bien. Un test que falla es el mejor prompt que le puedes dar a un agente: es preciso, es una comprobación computacional y no deja lugar a interpretación.
Pero (siempre hay un pero) los agentes también son sorprendentemente buenos haciendo trampas con los tests: aserciones que no comprueban nada, mockear justo lo que se está testeando o borrar un test que falla para "arreglar" la build. Todos los antipatrones que hemos visto en este artículo, producidos a velocidad de máquina.
Lo que significa que tu rol cambia: tu criterio para testear es ahora tu habilidad más valiosa. Revisa los tests que escribe un agente con más cuidado que el código de producción, porque esos tests son la especificación que seguirá el siguiente agente.
Para hacer ese criterio transferible, he destilado todo este artículo en una skill de Claude Code: create-test. Mis agentes la cargan cada vez que escriben tests: API pública, AAA, una aserción por test, funciones de setup. Cópiala, adáptala, hazla tuya.
“”Los agentes escriben los tests. Tú pones el criterio.
Y tú, ¿cómo estás testeando en la era de la IA? Si quieres más contenido como este, suscríbete a la newsletter, y siéntete libre de compartir conmigo tu enfoque.


