
Muchas veces, cuando cojo un proyecto heredado, me alegra ver que el código tiene una cobertura de tests alta y que rara vez falla. En ese momento resulta fácil pensar que estamos ante un terreno seguro, casi como si fuese un green field project, y que podremos mantenerlo y seguir añadiendo funcionalidades sin grandes sobresaltos.
Pero espera
La sensación cambia muy rápido cuando abrimos el primer fichero de tests y descubrimos que llevamos demasiado tiempo leyendo estructuras complejas, intentando entender datos auxiliares y perdiendo el foco sobre el comportamiento que realmente queremos validar.
No hace falta que el código sea heredado para vivir esta situación. También puede pasarnos con nuestros propios tests. Por eso merece la pena revisar algunas prácticas que ayudan a escribir tests más legibles y a mantener la atención sobre la intención.
El problema de partida
Imaginemos un componente funcional de React que muestra un mensaje concreto en función de una propiedad recibida.
type Exercise = {
tags: string[];
warning?: string;
};
function ExerciseCard({ exercise }: { exercise: Exercise }) {
return (
<>
{exercise.warning && <p>{exercise.warning}</p>}
{exercise.tags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</>
);
}
Al mirar el componente entendemos lo importante: recibe un exercise, ese objeto tiene tags y puede tener un warning que solo debería renderizarse cuando existe. La interfaz del componente es sencilla.
El problema suele aparecer en el fichero de test.
it('should display the warning', () => {
const exercise = {
tags: ['react', 'testing'],
warning: 'Do not skip warm up',
};
// mucho ruido adicional...
});
Aunque un test así pueda dar 100% de cobertura, eso no significa que sea agradable de leer. A veces tenemos decenas de líneas de preparación para validar tres comportamientos simples.
Nombres que expliquen la intención
Uno de los primeros síntomas de un test poco legible suele estar en su nombre. Expresiones como should display the warning o should not display the warning se quedan cortas porque no nos dicen en qué condición debería ocurrir eso.
Una mejora útil es apoyarnos en la idea de given-when-then para expresar mejor el contexto:
it('renders the warning when the exercise contains one', () => {
// ...
});
it('does not render the warning when the exercise has no warning', () => {
// ...
});
No hace falta escribir siempre el patrón completo de forma estricta, pero sí entender la lógica que hay detrás: el nombre del test debería explicar la situación y el resultado esperado.
Evitar detalles irrelevantes
También es fácil caer en descripciones demasiado específicas, por ejemplo afirmar que “muestra una lista de dos elementos” cuando lo importante no es que sean dos, sino que renderiza los tags. Ese tipo de detalle puede venir de una especificación antigua o de un intento de hacer el test “más preciso”, pero muchas veces solo añade ruido y confusión.
Ocultar estructura, no comportamiento
El siguiente paso consiste en esconder parte de la estructura de datos que ocupa espacio mental dentro del test. Según el lenguaje, podemos hacerlo con funciones auxiliares, clases falsas o builders.
En TypeScript, una solución cómoda es usar destructuring con valores por defecto:
type Exercise = {
tags: string[];
warning?: string;
};
function buildExercise(overrides: Partial<Exercise> = {}): Exercise {
return {
tags: ['react', 'testing'],
warning: undefined,
...overrides,
};
}
Con eso podemos construir un objeto completo con los valores por defecto o sobrescribir solo aquello que interesa en cada caso. Además, si la estructura cambia, el test deja de depender de todos los detalles internos del modelo.
El cuerpo del test debe respirar
Una vez tenemos un builder, el test pasa a centrarse en lo que quiere demostrar:
it('renders the warning when the exercise contains one', () => {
const exercise = buildExercise({ warning: 'Do not skip warm up' });
const wrapper = render(<ExerciseCard exercise={exercise} />);
expect(wrapper.getByText('Do not skip warm up')).toBeInTheDocument();
});
Ahora la lectura va directa a la intención. Ya no estamos navegando por una montaña de propiedades irrelevantes.
Helpers para el montaje
Cuando escribimos tests con frecuencia, es habitual pensar en beforeEach o beforeAll para centralizar el montaje del componente. Pero esas herramientas no siempre ayudan, especialmente si nos obligan a depender de un estado compartido o a recordar demasiado contexto implícito.
En muchos casos es más útil tener una pequeña función que monte el componente y devuelva las referencias comunes:
function renderExercise(exercise: Exercise) {
const view = render(<ExerciseCard exercise={exercise} />);
return {
...view,
getWarning: () => view.queryByText(/warm up/i),
};
}
Ese tipo de helper reduce repetición sin esconder el comportamiento importante.
Resultado
La diferencia no está solo en reducir líneas. La mejora real es que el test pasa a representar exactamente lo que necesitamos saber sobre el comportamiento del componente.
Cuando los tests son más legibles:
- cuesta menos añadir nuevas funcionalidades,
- es más fácil detectar duplicidades o malos nombres,
- y disminuye el esfuerzo de comprensión al volver al código semanas después.
Conclusión
Un test no es valioso solo porque cubra código, sino porque comunica intención. Si para entenderlo necesitamos leer una gran cantidad de estructura accidental, probablemente todavía no está haciendo bien su trabajo.
Usar nombres expresivos, builders y pequeños helpers de montaje ayuda a mantener el foco en el comportamiento y a evitar que el fichero de tests se convierta en una barrera de entrada para quien lo lea después.