Cómo escribir tu primer test unitario en JavaScript

Aprender a escribir un test unitario en JavaScript puede parecer un paso muy técnico al principio, especialmente si vienes de centrarte en crear interfaces, resolver funcionalidades o entregar cambios rápido. Sin embargo, cuanto antes incorporas el testing a tu forma de trabajar, antes empiezas a notar una diferencia importante: desarrollas con más seguridad, detectas errores antes y entiendes mejor cómo debería comportarse tu propio código.

Un test unitario no es una garantía absoluta de que una aplicación nunca fallará, pero sí es una herramienta muy útil para comprobar que una parte pequeña y concreta del código hace exactamente lo que esperas. En lugar de abrir el navegador, probar manualmente varios casos y confiar en que todo sigue funcionando, puedes escribir una prueba automatizada que verifique ese comportamiento por ti.

En este artículo vamos a ver cómo escribir tu primer test unitario en JavaScript desde cero, con una explicación clara, ejemplos sencillos y buenas prácticas para que no solo copies código, sino que entiendas qué estás haciendo. Si todavía estás construyendo una base sobre testing, también puede ayudarte leer esta guía sobre qué es el testing unitario y cuándo usarlo, donde se explica el concepto desde una perspectiva más general.

Qué es un test unitario en JavaScript

Un test unitario es una prueba automatizada que verifica el comportamiento de una unidad pequeña de código. Esa unidad puede ser una función, un método, una utilidad, una transformación de datos o una pieza lógica concreta de una aplicación.

Por ejemplo, si tienes una función que suma dos números, un test unitario puede comprobar que al pasarle 2 y 3, el resultado sea 5. Parece algo demasiado simple, pero esa sencillez es precisamente una de las claves del testing unitario: comprobar piezas pequeñas de forma aislada.

function sumar(a, b) {
  return a + b;
}

Un test unitario para esa función podría expresar algo como:

esperamos que sumar(2, 3) devuelva 5

La ventaja está en que esta comprobación se puede ejecutar todas las veces que quieras. Si más adelante alguien modifica la función y rompe su comportamiento, el test fallará y te avisará.

Diferencia entre probar manualmente y escribir tests

Cuando pruebas manualmente una funcionalidad, normalmente sigues una serie de pasos: abres la aplicación, haces clic, introduces datos y observas el resultado. Este proceso es necesario en muchos casos, pero también tiene límites. Puedes olvidarte de revisar un caso, repetir pruebas consume tiempo y es fácil asumir que algo funciona solo porque funcionó una vez.

En cambio, las pruebas unitarias JavaScript permiten automatizar comprobaciones pequeñas y repetibles. No sustituyen todo el trabajo de revisión, pero sí reducen la dependencia de la memoria y de la intuición.

Probar manualmente responde a la pregunta: “¿parece que esto funciona ahora?”.
Un test unitario responde a la pregunta: “¿esta parte concreta del código sigue comportándose como habíamos definido?”.

Si quieres ampliar la visión general, puedes complementar esta lectura con el artículo sobre tipos de testing en aplicaciones web, donde se explican las diferencias entre pruebas unitarias, de integración, end to end y otros enfoques.

Por qué deberías aprender testing JavaScript desde el principio

Muchas personas empiezan a interesarse por el testing JavaScript cuando el proyecto ya ha crecido, cuando hay miedo a tocar ciertas partes del código o cuando una corrección rompe otra funcionalidad sin querer. Aunque nunca es tarde para empezar, aprender testing desde el principio tiene varias ventajas.

La primera es que te ayuda a escribir código más claro. Para poder testear una función, esa función debe tener una responsabilidad concreta. Si una función hace demasiadas cosas, tiene dependencias ocultas o mezcla lógica con efectos secundarios, escribir un test se vuelve complicado. Por eso, el testing también actúa como una señal de diseño.

La segunda ventaja es que te da confianza al refactorizar. Refactorizar significa mejorar la estructura interna del código sin cambiar su comportamiento externo. Si tienes tests que verifican ese comportamiento, puedes reorganizar el código con menos miedo.

La tercera ventaja es que mejora la comunicación dentro de un equipo. Un test bien escrito funciona casi como documentación ejecutable. Explica qué se espera de una función y qué casos se consideran importantes.

Cuándo tiene sentido escribir un test unitario

No hace falta testear absolutamente todo. Un error común al empezar es pensar que cualquier línea de código debe tener una prueba asociada. En la práctica, conviene priorizar aquellas partes donde un fallo puede tener impacto real.

Tiene mucho sentido escribir tests unitarios para funciones que transforman datos, validaciones de formularios, cálculos de precios, utilidades reutilizables, reglas de negocio o casos límite que podrían romperse con facilidad.

Por ejemplo, si tienes una función que calcula el precio final de un producto con descuento, ahí un test unitario puede ahorrarte muchos problemas. Si el cálculo falla, el impacto puede ser importante.

Qué no suele ser ideal para un primer test unitario

Para empezar, no conviene elegir una parte demasiado compleja de la aplicación. Si tu primer test intenta cubrir una pantalla completa, una llamada a una API, una interacción con el DOM y varios estados al mismo tiempo, es probable que termines frustrada.

Para tu primer test unitario JavaScript, lo mejor es elegir una función pura: una función que recibe unos datos, devuelve un resultado y no depende de elementos externos. Este enfoque se relaciona directamente con la forma de trabajar explicada en la guía sobre cómo testear funciones puras en JavaScript.

Preparar el entorno para escribir tu primer test

Para escribir tests en JavaScript necesitas un entorno de ejecución y una herramienta de testing. En este artículo vamos a usar Jest, porque es una opción muy conocida, tiene una sintaxis accesible y permite empezar con pocos pasos.

También podrías usar Vitest, especialmente si trabajas con Vite, React o proyectos frontend modernos. La lógica de los primeros tests es muy parecida: defines un caso, ejecutas una función y compruebas el resultado esperado. Si tu proyecto ya usa Vite, te puede interesar esta guía específica sobre Vitest para proyectos modernos con Vite.

Crear un proyecto básico

Primero, crea una carpeta para el ejemplo:

mkdir primer-test-js
cd primer-test-js

Después inicializa un proyecto con npm:

npm init -y

Esto generará un archivo package.json, donde se guardará la configuración básica del proyecto y los scripts que podrás ejecutar desde la terminal.

Ahora instala Jest como dependencia de desarrollo:

npm install --save-dev jest

Una dependencia de desarrollo es una herramienta que necesitas para trabajar en el proyecto, pero que no forma parte necesariamente del código final que se ejecutará en producción.

Configurar el script de test

Abre el archivo package.json y busca la sección "scripts". Puedes modificarla así:

{
  "scripts": {
    "test": "jest"
  }
}

Con esto podrás ejecutar los tests usando:

npm test

Este comando le dirá a Jest que busque archivos de prueba en el proyecto y los ejecute.

Crear una función sencilla para testear

Vamos a empezar con una función muy simple. Crea un archivo llamado sumar.js:

function sumar(a, b) {
  return a + b;
}

module.exports = sumar;

Esta función recibe dos valores y devuelve la suma. Además, usamos module.exports para poder importarla desde el archivo de test.

Aunque el ejemplo sea básico, tiene algo importante: la función es predecible. Si le pasamos 2 y 3, esperamos 5. Si le pasamos -1 y 1, esperamos 0. Esta claridad facilita mucho el aprendizaje.

Qué significa testear una función

Testear una función no consiste solo en comprobar que “funciona”. Eso es demasiado general. Un buen test define un escenario concreto.

Por ejemplo:

  • Cuando sumo 2 + 3, el resultado debe ser 5.
  • Cuando sumo -1 + 1, el resultado debe ser 0.
  • Cuando sumo 0 + 0, el resultado debe ser 0.

Cada caso expresa una expectativa. Esa expectativa es el corazón de cualquier prueba.

Escribir tu primer test unitario en JavaScript

Ahora crea un archivo llamado sumar.test.js. Jest reconoce por defecto archivos que contienen .test.js o .spec.js, así que este nombre es una convención habitual.

Dentro del archivo escribe:

const sumar = require('./sumar');

test('suma dos números positivos correctamente', () => {
  expect(sumar(2, 3)).toBe(5);
});

Este es tu primer test unitario en JavaScript.

Vamos a descomponerlo.

La función test

La función test define un caso de prueba. Recibe dos argumentos principales:

test('descripción del comportamiento', () => {
  // comprobación
});

El primer argumento es una descripción. Conviene que sea clara y específica. No es lo mismo escribir:

test('funciona', () => {})

que escribir:

test('suma dos números positivos correctamente', () => {})

La segunda opción comunica mucho mejor qué comportamiento se está verificando.

La función expect

La función expect sirve para expresar el resultado que esperas obtener. En este caso:

expect(sumar(2, 3)).toBe(5);

Esto significa: “espero que el resultado de ejecutar sumar(2, 3) sea 5”.

toBe es un matcher, es decir, una función que compara el resultado recibido con el valor esperado. Para valores primitivos como números, strings o booleanos, toBe suele ser suficiente.

Ejecutar el test

Ahora ejecuta en la terminal:

npm test

Si todo está bien, Jest mostrará un mensaje indicando que el test ha pasado. Eso significa que la función se comporta como esperabas en ese caso concreto.

Añadir más casos de prueba

Un solo test está bien para empezar, pero normalmente una función debe comprobarse con varios escenarios. Vamos a ampliar el archivo sumar.test.js:

const sumar = require('./sumar');

test('suma dos números positivos correctamente', () => {
  expect(sumar(2, 3)).toBe(5);
});

test('suma un número negativo y uno positivo correctamente', () => {
  expect(sumar(-1, 1)).toBe(0);
});

test('suma dos ceros correctamente', () => {
  expect(sumar(0, 0)).toBe(0);
});

Ahora tienes tres pruebas. Cada una cubre un caso diferente.

Esto es importante porque una función puede funcionar en un caso y fallar en otro. Por eso, escribir pruebas unitarias JavaScript no va solo de confirmar el caso feliz, sino también de pensar en límites, variantes y posibles errores.

El patrón Arrange, Act, Assert

Una forma muy útil de estructurar tests es el patrón Arrange, Act, Assert:

  • Arrange: preparas los datos necesarios.
  • Act: ejecutas la función que quieres probar.
  • Assert: compruebas el resultado esperado.

El test anterior podría escribirse así:

test('suma dos números positivos correctamente', () => {
  // Arrange
  const a = 2;
  const b = 3;

  // Act
  const resultado = sumar(a, b);

  // Assert
  expect(resultado).toBe(5);
});

Para una función tan simple puede parecer demasiado largo, pero este patrón se vuelve muy útil cuando los tests crecen. Ayuda a mantener el orden y facilita la lectura.

Cuándo usar una versión más compacta

Si el test es evidente, una versión compacta está bien:

expect(sumar(2, 3)).toBe(5);

Pero si hay varios datos, condiciones o pasos intermedios, separar preparación, acción y comprobación mejora mucho la mantenibilidad.

Probar una función un poco más realista

La suma es útil para entender la mecánica, pero vamos a ver un ejemplo algo más cercano a un proyecto real. Imagina que tienes una función para aplicar un descuento a un precio.

Crea un archivo calcularPrecioFinal.js:

function calcularPrecioFinal(precio, descuento) {
  if (descuento < 0 || descuento > 100) {
    throw new Error('El descuento debe estar entre 0 y 100');
  }

  return precio - precio * (descuento / 100);
}

module.exports = calcularPrecioFinal;

Esta función recibe un precio y un porcentaje de descuento. Si el descuento no está entre 0 y 100, lanza un error.

Ahora crea el archivo calcularPrecioFinal.test.js:

const calcularPrecioFinal = require('./calcularPrecioFinal');

test('aplica un descuento del 20% correctamente', () => {
  expect(calcularPrecioFinal(100, 20)).toBe(80);
});

test('devuelve el mismo precio cuando el descuento es 0', () => {
  expect(calcularPrecioFinal(100, 0)).toBe(100);
});

test('devuelve 0 cuando el descuento es 100', () => {
  expect(calcularPrecioFinal(100, 100)).toBe(0);
});

test('lanza un error si el descuento es negativo', () => {
  expect(() => calcularPrecioFinal(100, -10)).toThrow('El descuento debe estar entre 0 y 100');
});

Este ejemplo ya muestra algo más interesante. No solo comprobamos resultados correctos, sino también un caso de error. Eso es esencial en el testing: no basta con probar lo que debería salir bien; también hay que comprobar cómo responde el código cuando recibe valores inválidos.

Por qué conviene probar los casos límite

Los casos límite son escenarios que se encuentran en los extremos de una regla. En el ejemplo anterior, los límites son 0 y 100, porque son los valores mínimo y máximo permitidos para el descuento.

Probar estos casos ayuda a detectar errores típicos como permitir valores que deberían rechazarse, rechazar valores válidos, calcular mal porcentajes o no controlar entradas inesperadas.

En aplicaciones reales, estos detalles pueden afectar a formularios, precios, permisos, fechas, filtros o validaciones importantes. Si estás planificando tests para una aplicación completa, puede resultarte útil el artículo sobre cómo crear una estrategia de testing para una aplicación web.

Buenas prácticas para escribir tu primer test unitario

Escribir tests no consiste en acumular archivos de prueba sin criterio. Un test útil debe ser claro, estable y fácil de mantener. De lo contrario, puede convertirse en una carga.

Escribe tests con nombres descriptivos

El nombre del test debe explicar el comportamiento esperado. Una buena descripción permite entender qué ha fallado sin tener que leer todo el código.

Mejor esto:

test('lanza un error si el descuento es mayor que 100', () => {
  expect(() => calcularPrecioFinal(100, 120)).toThrow();
});

Que esto:

test('error descuento', () => {
  expect(() => calcularPrecioFinal(100, 120)).toThrow();
});

El primer test comunica intención. El segundo obliga a interpretar.

Prueba comportamientos, no implementaciones

Un error frecuente es escribir tests demasiado pegados a cómo está construido el código internamente. Lo importante es comprobar qué hace la función, no necesariamente cómo lo hace.

Si una función devuelve el precio final correcto, el test no debería depender de si internamente usas una variable llamada total, una operación en una línea o varios pasos separados.

Esto permite refactorizar sin romper tests innecesariamente.

Mantén cada test enfocado

Un test debería comprobar una idea principal. Si en un mismo test verificas demasiadas cosas, cuando falle será más difícil saber cuál es el problema.

Por ejemplo, en lugar de escribir un único test enorme para todos los descuentos posibles, suele ser mejor crear varios tests pequeños con nombres claros.

Evita tests frágiles

Un test frágil es aquel que falla con facilidad aunque el comportamiento importante no se haya roto. Esto suele ocurrir cuando el test depende de detalles irrelevantes, del orden exacto de datos que no importa o de configuraciones externas innecesarias.

Para evitarlo, céntrate en la salida observable de la función y reduce las dependencias externas. Este punto conecta con muchas de las recomendaciones explicadas en buenas prácticas para escribir tests mantenibles.

Errores comunes al empezar con pruebas unitarias JavaScript

Cuando empiezas con pruebas unitarias JavaScript, es normal cometer algunos errores. Lo importante es identificarlos pronto para no convertirlos en hábitos.

Testear demasiado tarde

Si esperas a que todo el proyecto esté terminado para escribir tests, probablemente te resulte más difícil. El código puede estar muy acoplado, las funciones pueden tener demasiadas responsabilidades y quizá ni siquiera tengas claro qué comportamiento esperas en algunos casos.

No hace falta aplicar TDD desde el primer día, pero sí conviene escribir tests mientras desarrollas o justo después de terminar una pieza lógica.

Confundir test unitario con test de integración

Un test unitario prueba una unidad pequeña de código de forma aislada. Un test de integración comprueba cómo colaboran varias partes entre sí.

Por ejemplo, probar una función que calcula un descuento es unitario. Probar que un formulario recoge datos, los valida, llama a una API y actualiza la interfaz se acerca más a una prueba de integración o incluso a una prueba end to end, según el caso.

Ambos tipos de test son útiles, pero tienen objetivos diferentes.

Escribir tests que no aportan valor

No todos los tests son igual de útiles. Por ejemplo, testear que una función mockeada devuelve exactamente lo que tú mismo le has dicho que devuelva no aporta demasiado. Tampoco suele tener sentido testear detalles triviales que no contienen lógica real.

Un buen criterio es preguntarte: si este test falla, ¿me estaría avisando de un problema importante?

Si la respuesta es no, quizá ese test no sea prioritario.

Para profundizar en estos fallos habituales, puedes revisar también el artículo sobre errores comunes al empezar con testing en desarrollo frontend.

Cómo saber si tu primer test está bien escrito

Un buen primer test no tiene que ser perfecto. Tiene que ayudarte a entender la dinámica básica: preparar un escenario, ejecutar código y comprobar un resultado.

Aun así, puedes revisar algunos puntos:

  • El test tiene un nombre claro.
  • Comprueba un comportamiento concreto.
  • No depende de otros tests.
  • Se puede ejecutar varias veces con el mismo resultado.
  • Falla si rompes la función a propósito.
  • Es fácil de leer sin demasiada explicación adicional.

Una técnica muy sencilla es modificar temporalmente la función para que devuelva un resultado incorrecto. Por ejemplo:

function sumar(a, b) {
  return a - b;
}

Si el test falla, buena señal: significa que está comprobando algo real. Después, vuelve a dejar la función correcta.

Qué hacer cuando un test falla

Cuando un test falla, no lo veas como una molestia. Un test fallido es información. Te está diciendo que hay una diferencia entre el comportamiento esperado y el comportamiento actual.

Ante un fallo, revisa si el test está bien planteado, si el resultado esperado es correcto, si la función tiene un error o si ha cambiado un requisito y debes actualizar la prueba.

No todos los fallos significan que el código productivo está mal. A veces el test está desactualizado o mal definido. Lo importante es analizar el motivo antes de cambiar cosas a ciegas.

Jest, Vitest y otras herramientas de testing JavaScript

Para escribir un test unitario JavaScript necesitas una herramienta que ejecute las pruebas y te permita expresar expectativas. Jest es una opción muy popular y adecuada para empezar. Vitest, por su parte, se ha convertido en una alternativa muy cómoda en proyectos basados en Vite.

La buena noticia es que, para los primeros pasos, la sintaxis suele ser muy parecida:

test('describe un comportamiento', () => {
  expect(resultado).toBe(valorEsperado);
});

Esto significa que lo más importante no es memorizar una herramienta concreta, sino entender la mentalidad del testing: definir comportamientos esperados y comprobarlos de forma automática.

Cuál elegir para empezar

Si estás aprendiendo desde cero con JavaScript puro, Jest es una opción sencilla para practicar. Si ya trabajas en un proyecto con Vite, React o una configuración moderna de frontend, Vitest puede integrarse de forma más natural.

En cualquier caso, no te obsesiones con elegir la herramienta perfecta. Para tu primer test, lo importante es empezar con una función pequeña, escribir una prueba clara y ejecutarla desde la terminal.

Ejemplo completo de primer test unitario

Para recapitular, esta sería una estructura mínima de proyecto:

primer-test-js/
├── calcularPrecioFinal.js
├── calcularPrecioFinal.test.js
├── package.json
└── node_modules/

Archivo calcularPrecioFinal.js:

function calcularPrecioFinal(precio, descuento) {
  if (descuento < 0 || descuento > 100) {
    throw new Error('El descuento debe estar entre 0 y 100');
  }

  return precio - precio * (descuento / 100);
}

module.exports = calcularPrecioFinal;

Archivo calcularPrecioFinal.test.js:

const calcularPrecioFinal = require('./calcularPrecioFinal');

test('aplica un descuento del 20% correctamente', () => {
  expect(calcularPrecioFinal(100, 20)).toBe(80);
});

test('devuelve el mismo precio cuando el descuento es 0', () => {
  expect(calcularPrecioFinal(100, 0)).toBe(100);
});

test('lanza un error si el descuento es menor que 0', () => {
  expect(() => calcularPrecioFinal(100, -5)).toThrow('El descuento debe estar entre 0 y 100');
});

Comando para ejecutar:

npm test

Con esto ya tienes un ejemplo completo de testing JavaScript aplicado a una función sencilla, realista y fácil de entender.

Preguntas frecuentes sobre test unitario JavaScript

¿Qué debería testear primero en JavaScript?

Lo más recomendable es empezar por funciones puras y pequeñas. Por ejemplo, utilidades de formato, cálculos, validaciones o transformaciones de datos. Estas piezas son ideales para aprender porque reciben una entrada clara y devuelven una salida fácil de comprobar.

Evita empezar por componentes complejos, llamadas a APIs o lógica muy acoplada a la interfaz. Ya llegarás a eso más adelante.

¿Necesito saber mucho JavaScript para escribir tests unitarios?

No necesitas ser experta, pero sí conviene entender funciones, módulos, valores de retorno y errores básicos. Si sabes crear una función, exportarla, importarla y ejecutarla, ya tienes una buena base para escribir tu primer test unitario.

De hecho, escribir tests puede ayudarte a mejorar tu JavaScript porque te obliga a pensar con más precisión en entradas, salidas y comportamientos esperados.

¿Jest o Vitest: cuál es mejor para empezar?

Ambas herramientas son buenas. Jest es una opción muy extendida y fácil de encontrar en tutoriales, proyectos y documentación. Vitest suele encajar muy bien en proyectos modernos con Vite y ofrece una experiencia rápida e integrada.

Para aprender el concepto de pruebas unitarias JavaScript, cualquiera de las dos te sirve. Lo importante es no quedarte bloqueada en la elección de la herramienta y empezar con un caso sencillo.

Más allá del primer test: cómo seguir practicando

Una vez que escribes tu primer test, el siguiente paso es practicar con funciones un poco más variadas. Puedes crear pequeñas utilidades y probarlas.

Algunas ideas:

  • Una función que valide si un email tiene un formato básico correcto.
  • Una función que convierta minutos en horas y minutos.
  • Una función que calcule el total de un carrito.
  • Una función que filtre productos por categoría.
  • Una función que formatee una fecha.
  • Una función que determine si una contraseña cumple ciertos requisitos.

Lo ideal es que cada ejercicio tenga varios casos: uno normal, uno límite y uno inválido. Así empiezas a construir una mentalidad de testing más completa.

Por ejemplo, si validas una contraseña, no pruebes solo una contraseña correcta. Prueba también una demasiado corta, una sin números, una vacía y una con caracteres válidos.

Para organizar estos contenidos dentro de una visión más amplia, puedes leer la guía sobre la pirámide de testing y cómo organizar las pruebas de una aplicación.

Del “creo que funciona” al “sé que está probado”

Escribir tu primer test unitario en JavaScript no va solo de instalar Jest, crear un archivo .test.js y ejecutar npm test. Eso es la parte técnica. La parte importante es el cambio de mentalidad.

Cuando escribes tests, empiezas a definir con más claridad qué esperas de tu código. Dejas de confiar únicamente en la intuición o en pruebas manuales repetidas y empiezas a construir una red de seguridad. Esa red no elimina todos los errores, pero sí te ayuda a detectarlos antes y a trabajar con más confianza.

Al principio puede parecer lento. Es normal. Estás aprendiendo una forma nueva de pensar. Pero con la práctica, los tests se vuelven una parte natural del desarrollo. Te ayudan a refactorizar, a documentar comportamientos y a evitar que errores antiguos vuelvan a aparecer.

No necesitas empezar con una suite de testing perfecta. Empieza con una función pequeña. Escribe un test claro. Ejecútalo. Rómpelo a propósito. Arréglalo. Repite el proceso.

Ese primer test puede parecer simple, pero marca un antes y un después: pasas de preguntarte “creo que funciona” a poder decir “tengo una prueba que lo comprueba”.

Buenas prácticas para escribir tests mantenibles

Escribir tests es una de esas tareas que, al principio, pueden parecer bastante sencillas: eliges una herramienta, preparas un caso de prueba, haces algunas aserciones y compruebas que todo funciona. Sin embargo, cuando una aplicación crece, el verdadero reto no es solo tener tests, sino conseguir que esos tests sigan siendo útiles, claros y fáciles de mantener con el paso del tiempo.

Porque sí, un test puede pasar hoy y convertirse mañana en una fuente constante de ruido. Puede romperse por detalles irrelevantes, depender demasiado de la implementación interna, ser difícil de leer o necesitar tantos ajustes que el equipo termine viéndolo como una carga. Ahí es donde entran en juego las buenas prácticas de testing.

Hablar de tests mantenibles no significa escribir pruebas perfectas ni cubrir cada línea de código sin criterio. Significa crear una base de pruebas que ayude al equipo a trabajar con más seguridad, detectar errores importantes y evolucionar el producto sin miedo. En otras palabras, se trata de mejorar la calidad de tests para que el testing sea una herramienta real de confianza y no un trámite técnico.

Si estás empezando con este tema, puede resultarte útil leer primero la guía sobre testing unitario: qué es, cuándo usarlo y ejemplos prácticos, donde se explican las bases antes de profundizar en la mantenibilidad.

En este artículo veremos cómo escribir tests más claros, estables y sostenibles, especialmente en proyectos frontend, aunque muchas ideas también se aplican a backend, APIs o aplicaciones completas.

Qué significa realmente escribir tests mantenibles

Un test mantenible es aquel que sigue siendo comprensible y útil incluso cuando el código cambia. No está atado de forma innecesaria a detalles internos, no requiere reescrituras constantes y comunica con claridad qué comportamiento está validando.

La mantenibilidad en testing tiene mucho que ver con tres ideas principales: claridad, estabilidad y valor. Un test debe ser fácil de leer, debe fallar solo cuando hay un problema relevante y debe aportar información útil cuando algo no funciona.

Un error común es pensar que cuantos más tests tenga un proyecto, mejor será su calidad. Pero la cantidad no siempre equivale a confianza. Un proyecto puede tener cientos de pruebas y, aun así, ser difícil de mantener si esas pruebas están mal diseñadas. De hecho, una suite de tests demasiado frágil puede ralentizar al equipo, generar falsas alarmas y hacer que los desarrolladores pierdan confianza en el propio sistema de testing.

La pregunta importante no es solo “¿tenemos tests?”, sino: ¿estos tests nos ayudan a cambiar el código con seguridad?

Tests mantenibles frente a tests frágiles

Un test frágil es aquel que se rompe con facilidad aunque el comportamiento real de la aplicación no haya cambiado. Por ejemplo, si una prueba falla porque se modificó el nombre de una clase CSS que no afecta a la experiencia del usuario, probablemente ese test está demasiado acoplado a la implementación.

Un test mantenible, en cambio, se centra en el comportamiento observable. Valida lo que el usuario puede hacer, ver o experimentar. Esto es especialmente importante en frontend, donde muchas veces existe la tentación de comprobar detalles internos del componente en lugar de validar el resultado final.

Por ejemplo, en lugar de comprobar si una función interna fue llamada exactamente de una forma concreta, puede ser más útil comprobar si aparece un mensaje, si un botón se habilita o si se envía correctamente un formulario.

También es importante entender qué tipo de prueba necesitamos en cada caso. No es lo mismo validar una función aislada que comprobar un flujo completo de usuario. Para ampliar este enfoque, puedes consultar el artículo sobre tipos de testing en aplicaciones web: unitario, integración, end to end y más.

La mantenibilidad también es una decisión de equipo

La calidad de tests no depende solo de la herramienta elegida. También depende de los acuerdos del equipo. Cómo se nombran las pruebas, qué se considera importante testear, cuándo se aceptan mocks, cómo se organizan los archivos o qué nivel de cobertura tiene sentido son decisiones que deberían estar alineadas.

Sin criterios compartidos, cada persona puede escribir tests de una manera distinta. El resultado suele ser una suite irregular, difícil de leer y con estilos contradictorios. Por eso, una buena estrategia de testing también necesita documentación mínima, convenciones y revisiones de código donde los tests se valoren con el mismo rigor que el código de producción.

Principios básicos para mejorar la calidad de tests

Antes de entrar en técnicas concretas, conviene tener una base clara. Los tests mantenibles se apoyan en principios simples, pero muy poderosos. No se trata de aplicar reglas rígidas, sino de tomar mejores decisiones al escribir cada prueba.

Testea comportamientos, no detalles internos

Esta es una de las prácticas más importantes cuando hablamos de buenas prácticas testing. Un test debería validar qué hace el sistema desde el punto de vista del usuario o del consumidor de esa funcionalidad, no cómo lo hace internamente.

En frontend, esto significa priorizar pruebas que respondan a preguntas como:

¿El usuario puede iniciar sesión?
¿Se muestra un mensaje de error si el formulario está incompleto?
¿El botón queda deshabilitado mientras se envía la información?
¿La lista se actualiza después de aplicar un filtro?

En cambio, conviene evitar pruebas demasiado centradas en detalles como:

Qué estado interno exacto tiene un componente.
Qué método privado se ejecuta.
Qué clase CSS concreta se aplica si no es relevante para el comportamiento.
Qué estructura interna tiene el componente si el resultado visual o funcional no cambia.

Esto no significa que nunca puedas testear detalles técnicos. A veces tiene sentido hacerlo, especialmente en funciones puras, librerías internas o lógica compleja. De hecho, si estás trabajando con lógica aislada, puede interesarte profundizar en cómo testear funciones puras en JavaScript.

Como norma general, cuanto más se parezca el test al uso real de la aplicación, más valor tendrá.

Ejemplo de enfoque frágil

Un test frágil podría comprobar que un componente tiene una clase concreta:

expect(button.className).toBe('btn-primary active');

Este test puede romperse aunque el botón siga funcionando perfectamente. Bastaría con cambiar el nombre de la clase, usar CSS Modules, migrar a Tailwind CSS o reorganizar estilos.

Ejemplo de enfoque más mantenible

Un enfoque más sólido sería comprobar el comportamiento:

expect(screen.getByRole('button', { name: /enviar/i })).toBeEnabled();

Aquí el test valida algo más cercano a la experiencia del usuario: existe un botón con un nombre accesible y está habilitado.

Escribe tests fáciles de leer

Un buen test debería poder leerse casi como una pequeña historia. Alguien del equipo debería entender rápidamente qué escenario se está preparando, qué acción se ejecuta y qué resultado se espera.

Para conseguirlo, ayuda mucho seguir una estructura clara como Arrange, Act, Assert:

  • Arrange: prepara el escenario.
  • Act: ejecuta la acción.
  • Assert: comprueba el resultado.

Esta estructura no tiene que aparecer necesariamente como comentarios en todos los tests, pero sí debería sentirse en la organización del código.

test('muestra un mensaje de error cuando el email no es válido', async () => {
  render(<LoginForm />);

  await userEvent.type(screen.getByLabelText(/email/i), 'correo-invalido');
  await userEvent.click(screen.getByRole('button', { name: /entrar/i }));

  expect(screen.getByText(/introduce un email válido/i)).toBeInTheDocument();
});

Este test se entiende sin demasiada explicación. Se renderiza un formulario, se introduce un email incorrecto, se hace clic en el botón y se espera un mensaje de error. Esa claridad es clave para tener tests mantenibles.

Si todavía estás dando tus primeros pasos, también puedes revisar la guía sobre cómo escribir tu primer test unitario en JavaScript.

Nombra los tests con intención

El nombre de un test no debería describir la implementación, sino el comportamiento esperado. Un nombre como renderiza correctamente suele quedarse corto. Puede pasar hoy, mañana y dentro de seis meses, pero no explica qué está validando exactamente.

Es mejor usar nombres descriptivos:

test('muestra los productos filtrados cuando el usuario selecciona una categoría', () => {
  // ...
});

O también:

test('deshabilita el botón de envío mientras la petición está en curso', () => {
  // ...
});

Estos nombres ayudan a entender la intención del test incluso antes de leer el cuerpo. Además, cuando una prueba falla en la consola o en el pipeline de integración continua, el mensaje será mucho más útil.

Buenas prácticas de testing para evitar tests difíciles de mantener

Una vez que la base está clara, podemos revisar prácticas concretas que ayudan a reducir deuda técnica en los tests.

Evita el exceso de mocks

Los mocks son útiles, pero abusar de ellos puede hacer que las pruebas se alejen demasiado de la realidad. Si todo está mockeado, el test puede terminar validando una versión artificial del sistema que no representa cómo funciona la aplicación.

El problema no es usar mocks, sino usarlos sin criterio. Mockear una llamada externa, una API de terceros o un servicio que no quieres ejecutar en el entorno de pruebas puede ser razonable. Pero mockear cada función interna puede volver los tests más frágiles y menos representativos.

Una buena pregunta es: ¿este mock simplifica el test o está ocultando una integración importante?

Si el mock elimina justo aquello que deberías validar, quizá estás perdiendo valor. Por ejemplo, si quieres comprobar que un formulario muestra datos después de una petición, puede tener sentido simular la respuesta de la API. Pero si mockeas toda la lógica que transforma esos datos, puede que el test ya no esté comprobando el comportamiento completo.

No repitas configuración innecesaria

La repetición es uno de los grandes enemigos de la mantenibilidad. Si tienes diez tests con la misma preparación inicial, cada cambio futuro será más costoso.

Para evitarlo, puedes crear pequeñas funciones auxiliares. Por ejemplo:

function renderLoginForm() {
  const user = userEvent.setup();
  render(<LoginForm />);
  return { user };
}

Ahora cada test puede centrarse en el escenario concreto:

test('permite enviar el formulario con credenciales válidas', async () => {
  const { user } = renderLoginForm();

  await user.type(screen.getByLabelText(/email/i), 'marta@email.com');
  await user.type(screen.getByLabelText(/contraseña/i), '123456');
  await user.click(screen.getByRole('button', { name: /entrar/i }));

  expect(screen.getByText(/bienvenida/i)).toBeInTheDocument();
});

La clave está en no llevar la abstracción demasiado lejos. Una función auxiliar debe mejorar la legibilidad, no esconder lo importante. Si para entender el test tienes que saltar entre cinco archivos de helpers, probablemente la abstracción se ha vuelto excesiva.

Cuándo crear helpers de testing

Tiene sentido crear helpers cuando:

  • Hay una preparación repetida en muchos tests.
  • El helper representa una acción clara del usuario.
  • Reduce ruido sin ocultar el comportamiento principal.
  • Facilita cambios futuros en la configuración.

No tiene tanto sentido cuando:

  • Solo se usa una vez.
  • Hace demasiadas cosas a la vez.
  • Esconde datos importantes para entender el caso.
  • Obliga a aprender una API interna compleja solo para escribir tests.

Usa datos de prueba realistas, pero controlados

Los datos de prueba también influyen en la calidad de tests. Usar valores demasiado genéricos como test, foo o 123 puede hacer que el test sea menos expresivo. En cambio, usar datos realistas ayuda a entender mejor el escenario.

Por ejemplo, no es lo mismo esto:

const user = { name: 'test', email: 'test@test.com' };

Que esto:

const user = {
  name: 'Marta González',
  email: 'marta.gonzalez@example.com'
};

El segundo ejemplo comunica mejor el tipo de entidad con la que estamos trabajando. Aun así, los datos deben ser controlados. No conviene depender de datos externos, fechas reales cambiantes o respuestas impredecibles.

En proyectos grandes, pueden ser útiles los test data builders, es decir, funciones para crear datos de prueba con valores por defecto y modificar solo lo necesario en cada caso.

function createUser(overrides = {}) {
  return {
    id: 'user-1',
    name: 'Marta González',
    email: 'marta.gonzalez@example.com',
    role: 'editor',
    ...overrides
  };
}

Así puedes escribir:

const adminUser = createUser({ role: 'admin' });

Esto mejora la legibilidad y evita duplicación.

Cómo escribir tests más estables en frontend

En aplicaciones frontend, muchos tests se vuelven difíciles de mantener porque dependen demasiado del DOM, del tiempo, de animaciones, de estilos o de detalles que cambian con frecuencia. Por eso, conviene aplicar algunas buenas prácticas específicas.

Prioriza selectores accesibles

Cuando escribes tests con herramientas orientadas al comportamiento del usuario, como React Testing Library, es recomendable buscar elementos como lo haría una persona usuaria o una tecnología de asistencia.

Por ejemplo:

screen.getByRole('button', { name: /guardar/i });
screen.getByLabelText(/email/i);
screen.getByText(/perfil actualizado/i);

Este enfoque mejora la accesibilidad y hace que los tests sean más resistentes. Si un botón no tiene un nombre accesible, el test puede ayudarte a detectar un problema real de experiencia de usuario.

En cambio, depender constantemente de selectores como data-testid puede ser menos expresivo. Eso no significa que data-testid sea malo. Puede ser útil cuando no hay una alternativa accesible clara, pero no debería ser la primera opción en todos los casos.

Controla las operaciones asíncronas

Muchos tests frágiles aparecen por no manejar bien la asincronía. En frontend, es común trabajar con peticiones, estados de carga, validaciones diferidas o actualizaciones del DOM después de una interacción.

Si el test no espera correctamente, puede fallar de forma intermitente. Estos fallos, conocidos como flaky tests, son especialmente peligrosos porque reducen la confianza del equipo.

En lugar de hacer comprobaciones inmediatas cuando el resultado tarda en aparecer, conviene usar utilidades pensadas para esperar cambios:

expect(await screen.findByText(/datos cargados/i)).toBeInTheDocument();

También es importante evitar esperas arbitrarias como:

await new Promise(resolve => setTimeout(resolve, 1000));

Este tipo de solución suele ralentizar la suite y no garantiza estabilidad. Es mejor esperar a que ocurra una condición concreta.

Si trabajas con Vite, también puede ayudarte revisar la guía de Vitest para proyectos modernos con Vite, donde la configuración de entorno influye bastante en la estabilidad de las pruebas.

Reduce la dependencia de snapshots grandes

Los snapshots pueden ser útiles en algunos casos, pero abusar de ellos suele generar problemas. Un snapshot enorme es difícil de revisar y puede convertirse en algo que el equipo actualiza automáticamente sin analizar si el cambio es correcto.

Un buen snapshot debería ser pequeño, intencional y fácil de interpretar. Si el snapshot ocupa muchas líneas y mezcla estructura, estilos y contenido, quizá sea mejor sustituirlo por aserciones explícitas.

Por ejemplo, en lugar de validar todo el árbol del componente, puedes comprobar elementos clave:

expect(screen.getByRole('heading', { name: /mi cuenta/i })).toBeInTheDocument();
expect(screen.getByText(/plan premium/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancelar suscripción/i })).toBeEnabled();

Este enfoque suele generar tests más claros y mantenibles.

Organización de archivos y estructura de la suite

La forma en que organizas los tests también afecta a su mantenimiento. Una suite desordenada puede ser difícil de navegar, especialmente cuando el proyecto crece.

Ubica los tests cerca del código cuando tenga sentido

Una práctica habitual es colocar los tests cerca del archivo que prueban:

components/
  LoginForm/
    LoginForm.tsx
    LoginForm.test.tsx

Este enfoque facilita encontrar la prueba asociada a un componente. También ayuda a mantener el test actualizado cuando se modifica el código.

Otra opción es tener una carpeta separada de tests, especialmente para pruebas de integración o end to end:

tests/
  integration/
  e2e/

No hay una única respuesta correcta. Lo importante es que la estructura sea coherente y fácil de entender para el equipo.

Separa tipos de pruebas por intención

No todos los tests tienen el mismo objetivo. Una buena suite suele combinar diferentes niveles:

  • Tests unitarios para lógica aislada.
  • Tests de integración para comprobar cómo colaboran varias piezas.
  • Tests end to end para validar flujos completos de usuario.

El problema aparece cuando intentamos que un único tipo de test lo cubra todo. Por ejemplo, si todas las validaciones dependen de pruebas end to end, la suite puede volverse lenta y costosa de mantener. Si solo tenemos tests unitarios, quizá no detectemos errores de integración importantes.

La clave está en buscar equilibrio. Para tener tests mantenibles, conviene que cada prueba tenga una intención clara y esté en el nivel adecuado. Este enfoque conecta muy bien con la pirámide de testing y cómo organizar las pruebas de una aplicación.

Pregunta útil antes de escribir un test

Antes de escribir una prueba, puedes preguntarte:

¿Cuál es el fallo real que quiero detectar con este test?

Si no puedes responder con claridad, quizá el test no está bien planteado. Esta pregunta ayuda a evitar pruebas redundantes, demasiado superficiales o centradas en detalles poco relevantes.

Cómo evitar tests redundantes o de bajo valor

No todo merece ser testeado de la misma manera. Una parte importante de las buenas prácticas testing consiste en saber elegir.

No testees la librería o el framework

Un error frecuente es escribir tests que en realidad verifican que React, Vue, Angular o cualquier otra herramienta funciona como se espera. Por ejemplo, probar que un useState actualiza un valor puede no tener sentido si no estás validando un comportamiento propio de tu aplicación.

Tus tests deberían centrarse en la lógica de negocio, las reglas del producto y las interacciones relevantes. No necesitas demostrar que el framework renderiza correctamente un componente básico. Necesitas comprobar que tu componente permite al usuario completar una tarea, muestra información importante o gestiona correctamente un caso de error.

Evita perseguir cobertura sin criterio

La cobertura de código puede ser una métrica útil, pero no debería convertirse en el objetivo principal. Alcanzar un porcentaje alto de cobertura no garantiza que los tests sean buenos.

Puedes tener un 95% de cobertura y aun así no validar los escenarios críticos. También puedes tener una cobertura más moderada y una suite mucho más valiosa si los tests están bien pensados.

La cobertura sirve como señal, no como garantía. Lo importante es revisar qué partes están cubiertas, qué riesgos se están mitigando y qué escenarios siguen sin protección.

Elimina o reescribe tests que ya no aportan valor

Los tests también necesitan mantenimiento. A veces, una prueba que tenía sentido hace meses deja de ser útil porque el comportamiento cambió, el flujo desapareció o la funcionalidad fue simplificada.

Mantener tests obsoletos solo por conservar cobertura puede aumentar el ruido. Si una prueba ya no representa una necesidad real del producto, conviene eliminarla o reescribirla.

Una suite saludable no es aquella que nunca cambia, sino aquella que evoluciona junto con el proyecto.

Buenas prácticas para que los tests ayuden al equipo

Los tests no existen solo para las máquinas. También son una herramienta de comunicación entre personas. Un buen test explica cómo debería comportarse el sistema y ayuda a entender decisiones del producto.

Usa los tests como documentación viva

Cuando están bien escritos, los tests funcionan como documentación ejecutable. Pueden mostrar qué ocurre cuando un usuario introduce datos incorrectos, qué permisos tiene cada rol o cómo responde la interfaz ante un error del servidor.

Esto es especialmente útil en equipos donde la documentación tradicional queda desactualizada con facilidad. Los tests, en cambio, se ejecutan. Si el comportamiento cambia y el test falla, el equipo tiene una señal inmediata.

Por eso conviene escribir pruebas que sean expresivas. No solo deben comprobar que algo funciona; también deben contar qué comportamiento se espera.

Incluye los tests en las revisiones de código

En muchas revisiones de código se mira con detalle la implementación, pero se revisan los tests de forma superficial. Esto es un error. Los tests deberían evaluarse con preguntas como:

¿El test valida un comportamiento relevante?
¿Es fácil de entender?
¿Está demasiado acoplado a la implementación?
¿Puede fallar de forma intermitente?
¿Hay duplicación innecesaria?
¿El nombre describe bien el escenario?

Revisar los tests con criterio mejora la calidad del proyecto y ayuda a compartir conocimiento dentro del equipo.

Cuida los mensajes de error

Cuando un test falla, debería ser fácil entender qué ha pasado. Las aserciones claras ayudan mucho. Un error confuso puede hacer perder tiempo al equipo, especialmente en suites grandes.

Por ejemplo, es preferible una prueba que falle indicando que no se encontró un botón con cierto nombre accesible que una prueba que simplemente diga que un objeto no coincide con otro objeto enorme.

Cuanto más claro sea el fallo, más rápido podrá resolverse.

Cómo mantener una suite de tests sana a largo plazo

Escribir buenos tests es importante, pero mantener una suite saludable durante meses o años requiere disciplina.

Ejecuta los tests de forma frecuente

Los tests son más útiles cuando se ejecutan a menudo. Lo ideal es que el equipo pueda lanzarlos en local durante el desarrollo y que también se ejecuten automáticamente en integración continua.

Esto permite detectar errores pronto. Cuanto más tarde se descubre un fallo, más costoso suele ser entenderlo y corregirlo.

También conviene separar pruebas rápidas y lentas. Si toda la suite tarda demasiado, los desarrolladores pueden dejar de ejecutarla en local. Una buena organización permite validar rápidamente lo esencial y reservar pruebas más pesadas para momentos concretos del pipeline.

Investiga los tests intermitentes

Un test que falla a veces y pasa otras veces no debería ignorarse. Los flaky tests son una de las mayores amenazas para la confianza en la suite.

Cuando el equipo se acostumbra a reintentar pipelines hasta que pasan, el testing pierde autoridad. Por eso, un test intermitente debe investigarse cuanto antes. Puede deberse a asincronía mal gestionada, dependencias externas, datos compartidos, problemas de orden de ejecución o condiciones de carrera.

La solución no debería ser simplemente aumentar tiempos de espera. Lo importante es encontrar la causa y hacer que el test sea determinista.

Refactoriza los tests igual que refactorizas el código

Los tests también forman parte del proyecto. Si se permite que acumulen duplicación, nombres confusos o estructuras difíciles de seguir, tarde o temprano se convertirán en deuda técnica.

Refactorizar tests puede incluir:

  • Mejorar nombres.
  • Extraer helpers sencillos.
  • Eliminar duplicación.
  • Reorganizar archivos.
  • Sustituir snapshots grandes.
  • Reescribir pruebas demasiado acopladas.
  • Eliminar tests obsoletos.

La idea no es dedicar semanas enteras solo a limpiar tests, sino incluir pequeñas mejoras de forma continua.

Errores comunes al escribir tests mantenibles

Incluso con buenas intenciones, es fácil caer en patrones que dificultan el mantenimiento. Identificar estos errores ayuda a mejorar la suite antes de que se convierta en un problema mayor.

Escribir tests demasiado largos

Un test demasiado largo suele intentar validar demasiadas cosas a la vez. Esto hace que sea más difícil entender qué está fallando cuando algo se rompe.

Es mejor dividir escenarios complejos en pruebas más pequeñas, siempre que cada una tenga valor por sí misma. Un test debería tener una intención clara.

Usar nombres ambiguos

Nombres como should work, renders component o test login aportan poca información. Un buen nombre debe explicar el comportamiento esperado.

Por ejemplo:

test('muestra un aviso cuando la contraseña tiene menos de 8 caracteres', () => {
  // ...
});

Este nombre comunica el escenario y el resultado esperado.

Depender del orden entre tests

Cada test debería poder ejecutarse de forma independiente. Si una prueba depende de que otra haya creado datos, modificado un estado global o dejado cierta configuración preparada, la suite será más frágil.

Los tests independientes son más fáciles de depurar, paralelizar y mantener.

No actualizar los tests cuando cambia el producto

A veces se modifica una funcionalidad, se ajusta el diseño o cambia una regla de negocio, pero los tests se actualizan de forma apresurada. Esto puede generar pruebas incoherentes que ya no representan el comportamiento esperado.

Cuando cambia el producto, los tests deben revisarse con la misma atención que el código. No basta con hacer que vuelvan a pasar; hay que comprobar que siguen validando lo correcto.

Checklist de buenas prácticas para escribir tests mantenibles

Antes de cerrar un test o una nueva suite, puedes revisar esta lista:

  • ¿El test valida un comportamiento relevante?
  • ¿El nombre explica claramente el escenario?
  • ¿La estructura es fácil de leer?
  • ¿Evita depender de detalles internos innecesarios?
  • ¿Usa selectores accesibles cuando es posible?
  • ¿Gestiona bien la asincronía?
  • ¿Evita mocks excesivos?
  • ¿Tiene datos de prueba claros y controlados?
  • ¿Puede ejecutarse de forma independiente?
  • ¿Aporta confianza real al equipo?

No hace falta que cada test sea perfecto, pero sí conviene que la suite avance en esta dirección. La mantenibilidad se construye con pequeñas decisiones consistentes.

Preguntas frecuentes sobre tests mantenibles

¿Qué son los tests mantenibles?

Los tests mantenibles son pruebas que siguen siendo útiles, claras y fáciles de modificar a medida que evoluciona el proyecto. No están demasiado acoplados a detalles internos y validan comportamientos relevantes para la aplicación o para el usuario.

Un test mantenible no solo comprueba que algo funciona; también ayuda a entender qué se espera del sistema. Por eso, suele tener un nombre descriptivo, una estructura clara y aserciones fáciles de interpretar.

¿Cuál es la mejor práctica para mejorar la calidad de tests?

Una de las mejores prácticas es testear comportamientos en lugar de detalles de implementación. Esto reduce la fragilidad de la suite y hace que los tests representen mejor el uso real de la aplicación.

También es importante escribir nombres claros, evitar duplicación innecesaria, controlar bien la asincronía y revisar los tests como parte del proceso normal de desarrollo.

¿Es obligatorio tener una cobertura de tests muy alta?

No necesariamente. La cobertura puede ser útil como referencia, pero no garantiza por sí sola una buena calidad de tests. Lo importante es cubrir los flujos críticos, las reglas de negocio y los escenarios donde un error tendría impacto real.

Una cobertura alta con tests superficiales puede dar una falsa sensación de seguridad. En cambio, una suite bien enfocada, aunque no cubra el 100% del código, puede aportar mucha más confianza.

Más allá de pasar en verde: la verdadera calidad de un test

Escribir tests mantenibles no consiste únicamente en conseguir que la consola muestre todos los checks en verde. Eso es importante, claro, pero no suficiente. La verdadera calidad de un test está en su capacidad para acompañar la evolución del proyecto sin convertirse en un obstáculo.

Un buen test protege comportamientos importantes, comunica intención y ayuda al equipo a tomar decisiones con más seguridad. Un mal test, en cambio, puede generar ruido, ralentizar cambios y hacer que las personas desarrolladoras terminen desconfiando de la suite.

Por eso, las buenas prácticas testing no deberían verse como una capa extra de perfeccionismo, sino como una inversión en sostenibilidad. Cada nombre claro, cada aserción bien pensada y cada dependencia innecesaria que evitamos hacen que el proyecto sea un poco más fácil de mantener.

Al final, los tests mantenibles no son los que nunca cambian. Son los que pueden cambiar sin romper la confianza del equipo. Y esa confianza es, probablemente, una de las mejores señales de calidad que puede tener una aplicación.

Errores comunes al empezar con testing en desarrollo frontend

Empezar con testing frontend suele generar una mezcla curiosa de entusiasmo, dudas y cierta frustración. Por un lado, sabemos que las pruebas ayudan a construir aplicaciones más estables, reducen regresiones y aportan confianza antes de desplegar. Por otro, cuando nos sentamos por primera vez delante de un archivo .test.js, .spec.ts o una configuración de Jest, Vitest, Testing Library, Cypress o Playwright, es normal sentir que hay demasiadas decisiones que tomar.

¿Qué debo probar? ¿Cuándo conviene hacer una prueba unitaria? ¿Qué diferencia real hay entre una prueba de integración y una end to end? ¿Es necesario probar todos los componentes? ¿Estoy escribiendo pruebas útiles o solo aumentando el porcentaje de cobertura?

La realidad es que muchos de los errores testing frontend no aparecen por falta de interés, sino por empezar sin una estrategia clara. A veces se prueban detalles internos que no aportan confianza. Otras veces se abusa de los mocks, se escriben pruebas demasiado frágiles o se intenta alcanzar una cobertura del 100% sin pensar en el valor real de cada test.

Si todavía estás ubicando conceptos, puede ayudarte leer primero esta guía sobre tipos de testing en aplicaciones web, donde se explican las diferencias entre pruebas unitarias, integración, end to end y otros enfoques habituales.

En este artículo veremos los errores más comunes al empezar con pruebas frontend, por qué ocurren y cómo evitarlos desde el principio. La idea no es complicar el proceso, sino ayudarte a construir una base sólida para que el testing se convierta en una herramienta útil, no en una carga más dentro del proyecto.

Por qué el testing frontend suele parecer más difícil al principio

El testing frontend tiene una particularidad importante: no solo estamos verificando funciones aisladas, sino interfaces que cambian, interacciones de usuario, estados visuales, llamadas a APIs, navegación, formularios, validaciones, accesibilidad y comportamientos asincrónicos.

A diferencia de una función pura que recibe datos y devuelve un resultado, un componente frontend puede depender de muchas cosas a la vez: props, estado interno, contexto global, rutas, eventos, estilos, datos remotos y condiciones del navegador.

Por eso, cuando empezamos, es fácil caer en la tentación de probarlo todo de golpe o, al contrario, evitar las pruebas porque parecen demasiado complejas.

Sin embargo, el objetivo del testing no es demostrar que cada línea de código existe. El objetivo es ganar confianza en que la aplicación funciona como se espera para las personas que la van a usar.

El primer cambio de mentalidad: probar comportamiento, no implementación

Uno de los errores más frecuentes es pensar que una prueba debe confirmar cómo está construido un componente por dentro. Por ejemplo, verificar si se llama a una función interna concreta, si existe una clase CSS determinada o si el estado cambia con un nombre específico.

Este enfoque suele generar pruebas muy frágiles. Si mañana refactorizas el componente, cambias el nombre de una función o reorganizas la estructura interna, la prueba puede fallar aunque la experiencia de usuario siga funcionando perfectamente.

En frontend, una buena pregunta inicial sería: “¿Qué debería poder hacer o ver la persona usuaria?”. A partir de ahí, la prueba gana sentido.

Por ejemplo, en lugar de probar que un componente LoginForm ejecuta una función interna llamada handleSubmit, puede ser más útil probar que:

  • el usuario puede escribir su email;
  • el usuario puede escribir su contraseña;
  • al enviar el formulario aparece un mensaje de error si los datos no son válidos;
  • al enviar datos correctos se muestra una respuesta esperada o se produce una navegación.

Ese tipo de prueba se acerca más al uso real de la interfaz y aporta más confianza.

Error 1: Empezar sin saber qué tipos de pruebas existen

Uno de los grandes problemas al iniciarse en pruebas frontend es utilizar la palabra “testing” como si todo fuera lo mismo. Pero no todas las pruebas tienen el mismo propósito, ni el mismo coste, ni el mismo nivel de confianza.

Pruebas unitarias

Las pruebas unitarias verifican piezas pequeñas y aisladas de código. En frontend suelen utilizarse para funciones puras, utilidades, hooks reutilizables o lógica que puede probarse sin renderizar una interfaz completa.

Son rápidas, fáciles de ejecutar y muy útiles cuando tienes lógica de negocio que no depende directamente del DOM. Si quieres profundizar en este punto, puedes ampliar información en el artículo sobre testing unitario: qué es, cuándo usarlo y ejemplos prácticos.

Pruebas de integración

Las pruebas de integración comprueban cómo colaboran varias piezas. Por ejemplo, un formulario que renderiza varios campos, valida datos, muestra errores y ejecuta una acción al enviarse.

En frontend, muchas pruebas realmente valiosas caen en esta categoría. No prueban solo una función aislada, sino una interacción más completa entre componentes, estado y eventos.

Pruebas end to end

Las pruebas end to end verifican flujos completos desde el punto de vista del usuario. Por ejemplo: entrar en la web, iniciar sesión, añadir un producto al carrito y finalizar una compra.

Aportan mucha confianza, pero también suelen ser más lentas, más costosas de mantener y más sensibles a cambios externos.

No todas las pruebas tienen que estar en el mismo nivel

Un error habitual es intentar resolverlo todo con un único tipo de prueba. Hay equipos que escriben demasiadas pruebas unitarias y luego descubren que la aplicación falla al integrar los componentes. Otros se apoyan casi exclusivamente en pruebas end to end y terminan con una suite lenta y difícil de mantener.

Lo ideal es combinar niveles de prueba según el riesgo y la importancia de cada parte del proyecto.

Error 2: Perseguir cobertura sin pensar en valor real

La cobertura de código puede ser útil, pero también puede convertirse en una trampa. Uno de los errores testing frontend más comunes es obsesionarse con alcanzar un número concreto: 80%, 90% o incluso 100%.

El problema es que una cobertura alta no garantiza que las pruebas sean buenas. Puedes tener muchas líneas ejecutadas durante los tests y, aun así, no estar validando comportamientos importantes.

Por ejemplo, una prueba puede renderizar un componente y comprobar que no se rompe, pero no verificar si el botón funciona, si el mensaje de error aparece correctamente o si el formulario impide enviar datos inválidos.

La cobertura debe interpretarse como una señal, no como un objetivo absoluto.

Qué deberías priorizar antes que el porcentaje

Antes de perseguir una cifra, conviene preguntarse:

  • ¿Qué partes de la aplicación son más críticas?
  • ¿Qué errores serían más costosos en producción?
  • ¿Qué flujos usa más la persona usuaria?
  • ¿Qué lógica cambia con frecuencia?
  • ¿Qué componentes han provocado bugs antes?

Una estrategia madura de testing frontend empieza por el riesgo y el valor, no por el número. Para trabajar este enfoque con más orden, puedes apoyarte en esta guía sobre cómo crear una estrategia de testing para una aplicación web.

Error 3: Probar detalles internos del componente

Este error aparece muchísimo al empezar. Imagina que tienes un componente con un estado interno llamado isOpen. Una prueba frágil podría comprobar si isOpen cambia de false a true. Pero desde el punto de vista del usuario, ese estado no existe. Lo que existe es un menú que se abre, un modal que aparece o una sección que se muestra.

Cuando pruebas detalles internos, tus tests quedan demasiado acoplados a la implementación. Eso significa que cualquier refactor puede romperlos aunque la funcionalidad siga intacta.

Mejor enfoque: comprobar lo que se ve y se puede hacer

En lugar de preguntar “¿cambió el estado?”, pregunta:

¿Aparece el contenido esperado cuando hago clic?
¿El botón queda deshabilitado cuando el formulario no es válido?
¿Se muestra el mensaje correcto después de enviar los datos?

Este cambio parece pequeño, pero transforma por completo la calidad de las pruebas frontend.

Una buena prueba debería sobrevivir a un refactor razonable

Si cambias la estructura interna de un componente pero la experiencia sigue siendo la misma, la prueba no debería romperse. Esa es una buena señal de que estás probando comportamiento y no implementación.

Error 4: Abusar de los mocks

Los mocks son herramientas muy útiles, pero también pueden convertirse en un problema. Sirven para simular dependencias externas, respuestas de APIs, funciones complejas o comportamientos difíciles de reproducir en un entorno de prueba.

El problema aparece cuando se mockea demasiado.

Si todo está simulado, la prueba puede terminar verificando un mundo artificial que no se parece a la aplicación real. En ese caso, el test pasa, pero no aporta demasiada confianza.

Cuándo tiene sentido usar mocks

Los mocks pueden ser adecuados cuando:

  • quieres evitar llamadas reales a una API;
  • necesitas controlar una respuesta concreta;
  • estás probando un caso de error difícil de provocar;
  • una dependencia externa ralentiza o vuelve inestable la prueba;
  • quieres aislar una parte específica de la lógica.

Cuándo deberías tener cuidado

Conviene revisar el enfoque si estás mockeando:

  • componentes internos sin necesidad;
  • funciones que forman parte del comportamiento principal;
  • demasiadas capas a la vez;
  • respuestas tan simplificadas que no representan casos reales;
  • lógica que sería mejor probar mediante una integración.

En testing frontend, los mocks deben ayudarte a controlar el escenario, no a sustituir la realidad completa de la aplicación.

Error 5: No limpiar el estado entre pruebas

Otro error frecuente es permitir que una prueba afecte a la siguiente. Esto puede ocurrir con mocks, datos globales, localStorage, sessionStorage, temporizadores, contexto compartido o configuraciones modificadas durante un test.

Cuando esto pasa, aparecen pruebas impredecibles: a veces pasan, a veces fallan, a veces dependen del orden de ejecución. Este tipo de problema es especialmente frustrante porque da la sensación de que la suite de testing no es fiable.

Señales de que tienes estado compartido accidental

Puedes sospechar de este problema si:

  • una prueba falla solo cuando se ejecuta junto a otras;
  • una prueba pasa de forma aislada pero falla en la suite completa;
  • los resultados cambian según el orden de ejecución;
  • necesitas reiniciar constantemente el entorno;
  • los mocks conservan llamadas anteriores.

Una buena práctica es limpiar o restaurar mocks, almacenamiento y estados compartidos antes o después de cada prueba. No es una tarea glamourosa, pero evita muchos dolores de cabeza.

Error 6: Escribir pruebas demasiado largas y difíciles de leer

Una prueba también es código. Y como cualquier código, puede volverse confusa, repetitiva o difícil de mantener.

Al empezar, es común escribir tests enormes que preparan muchos datos, ejecutan demasiadas acciones y contienen varias expectativas mezcladas. El resultado es una prueba que cuesta entender y que falla con mensajes poco claros.

La estructura Arrange, Act, Assert

Una forma sencilla de mejorar la legibilidad es organizar la prueba en tres bloques mentales:

Arrange: preparar el escenario.
Act: ejecutar la acción principal.
Assert: comprobar el resultado esperado.

No siempre hace falta escribir comentarios con estos nombres, pero sí conviene mantener esa estructura en la cabeza.

Por ejemplo:

Primero preparas el componente con los datos necesarios.
Después simulas la interacción del usuario.
Por último compruebas qué debería ocurrir.

Una prueba debería contar una pequeña historia

Una buena prueba se lee casi como una frase:

“Cuando una persona introduce un email inválido y envía el formulario, entonces aparece un mensaje de error.”

Si al leer el test no puedes explicar rápidamente qué comportamiento valida, probablemente necesita simplificarse.

Para seguir profundizando en este punto, te puede interesar el artículo sobre buenas prácticas para escribir tests mantenibles.

Error 7: No probar los estados de carga, error y vacío

Muchas aplicaciones frontend se centran en mostrar datos. Pero esos datos no siempre están disponibles de inmediato. Puede haber estados de carga, errores de red, respuestas vacías o permisos insuficientes.

Uno de los errores más comunes es probar únicamente el caso feliz: cuando todo funciona bien.

El problema es que las personas usuarias no solo interactúan con la aplicación cuando todo va perfecto. También ven loaders, mensajes de error, pantallas vacías y formularios incompletos.

Estados que conviene contemplar

En una estrategia básica de testing frontend, deberías considerar:

  • estado inicial;
  • estado de carga;
  • estado con datos;
  • estado sin resultados;
  • estado de error;
  • estado deshabilitado;
  • validaciones incorrectas;
  • permisos o accesos restringidos.

No todos los componentes necesitan cubrir todos estos casos, pero los flujos importantes sí deberían contemplarlos.

Error 8: Ignorar la accesibilidad en las pruebas frontend

La accesibilidad no debería ser un añadido final. En frontend, muchas buenas prácticas de testing ya se acercan de forma natural a una interfaz más accesible.

Por ejemplo, cuando buscas un botón por su nombre accesible, un campo por su etiqueta o una alerta por su rol, estás probando de una forma más cercana a cómo interactúan muchas tecnologías de asistencia.

Testing y accesibilidad pueden reforzarse

Si una prueba no puede encontrar un input porque no tiene label, quizá no solo tienes un problema de test. Puede que también tengas un problema de accesibilidad.

Si un botón solo se identifica por una clase CSS, quizá la interfaz no está comunicando bien su propósito.

El testing frontend puede ayudarte a detectar estos detalles antes de que lleguen a producción.

No confundas test accesible con auditoría completa

Eso sí, probar con queries accesibles no sustituye una auditoría de accesibilidad. Son capas complementarias. Las pruebas ayudan a prevenir errores frecuentes, pero no reemplazan la revisión manual, el uso de herramientas especializadas ni las pruebas con tecnologías de asistencia.

Error 9: Crear pruebas frágiles por depender demasiado del texto exacto

Probar textos visibles es útil, pero también puede generar fragilidad si se hace sin criterio. Si una prueba falla cada vez que cambias una coma, un acento o una microcopy, quizá está demasiado acoplada al contenido literal.

La solución no es dejar de comprobar textos, sino distinguir entre textos críticos y textos secundarios.

Cuándo sí importa el texto exacto

Tiene sentido comprobar el texto exacto cuando:

  • es un mensaje legal;
  • es una validación importante;
  • es un error que debe entenderse claramente;
  • forma parte de una conversión;
  • afecta a la accesibilidad;
  • confirma una acción relevante.

Cuándo puedes ser más flexible

Puedes usar comprobaciones menos rígidas cuando el texto es decorativo, introductorio o susceptible de cambios frecuentes. Lo importante es que el test proteja el comportamiento, no que bloquee cualquier ajuste editorial.

Error 10: No integrar las pruebas en el flujo de trabajo

Escribir pruebas una vez y olvidarlas no sirve de mucho. Para que las pruebas frontend aporten valor, deben formar parte del flujo habitual de desarrollo.

Esto implica ejecutarlas en local, revisarlas durante el desarrollo, incluirlas en el proceso de revisión de código y automatizarlas en integración continua cuando el proyecto lo requiera.

Testing como hábito, no como fase final

Un error muy habitual es dejar las pruebas para el final. El problema es que, cuando el proyecto ya está avanzado, escribir tests se vuelve más difícil. Hay más deuda técnica, más dependencias, más incertidumbre y menos tiempo.

En cambio, si incorporas el testing poco a poco, cada nueva funcionalidad puede venir acompañada de sus casos principales.

No se trata de probarlo todo desde el primer día. Se trata de crear una cultura en la que cada cambio importante venga acompañado de una mínima red de seguridad.

Cómo empezar con testing frontend sin bloquearte

Si estás empezando, no necesitas montar la estrategia perfecta desde el primer día. De hecho, intentar hacerlo puede ser contraproducente.

Una forma razonable de comenzar es elegir una parte importante de la aplicación y escribir pruebas para los comportamientos más críticos.

Paso 1: Elige un flujo importante

Puede ser un login, un formulario de contacto, un buscador, un proceso de compra, una pantalla de configuración o cualquier interacción clave.

Paso 2: Define los casos principales

Piensa en el comportamiento esperado:

  • ¿qué debe poder hacer la persona usuaria?
  • ¿qué errores pueden aparecer?
  • ¿qué estados debería ver?
  • ¿qué resultado confirma que todo ha funcionado?

Paso 3: Escribe pocas pruebas, pero útiles

Es mejor tener cinco pruebas claras que veinte pruebas frágiles. Al principio, la prioridad debe ser aprender a escribir tests mantenibles.

Si estás dando tus primeros pasos, también puedes empezar con algo más acotado, como se explica en la guía sobre cómo escribir tu primer test unitario en JavaScript.

Paso 4: Refactoriza también tus pruebas

Las pruebas no son intocables. Si una prueba se vuelve difícil de entender, se repite demasiado o falla por motivos irrelevantes, también necesita refactor.

Herramientas habituales para empezar con pruebas frontend

Aunque este artículo se centra en los errores de enfoque, es normal preguntarse qué herramientas utilizar. La respuesta depende del stack, del equipo y del tipo de prueba que quieras escribir.

En proyectos modernos con Vite, una opción muy habitual es trabajar con Vitest por su integración rápida y su experiencia de desarrollo fluida. Puedes ampliar este tema en la guía sobre Vitest para proyectos modernos con Vite.

Si trabajas con TypeScript, también conviene pensar en cómo combinar tipado estático y pruebas automatizadas. El tipado ayuda a prevenir muchos errores, pero no sustituye los tests de comportamiento. Para profundizar en esta relación, puedes leer el artículo sobre testing en TypeScript: ventajas, configuración y buenas prácticas.

No elijas herramientas antes de entender qué necesitas probar

Un error habitual es empezar por la herramienta antes que por la estrategia. Jest, Vitest, Testing Library, Cypress o Playwright pueden ser muy útiles, pero ninguna herramienta compensa una mala decisión sobre qué probar.

Antes de instalar dependencias, conviene definir:

  • qué partes de la aplicación son más críticas;
  • qué tipo de errores quieres prevenir;
  • qué flujos necesitan más confianza;
  • qué pruebas deberían ejecutarse en local;
  • qué pruebas deberían formar parte del despliegue.

La herramienta importa, pero el criterio importa más.

Buenas prácticas para evitar los errores testing frontend más comunes

Después de revisar los errores principales, podemos resumir algunas recomendaciones prácticas.

Prioriza comportamiento visible

Pregúntate siempre qué ve, hace o espera la persona usuaria. Esa respuesta debería guiar tus pruebas.

Evita acoplarte a la implementación

No bases tus tests en nombres internos, estructuras privadas o clases CSS que pueden cambiar sin afectar al comportamiento.

Controla los mocks

Usa mocks cuando aporten claridad, velocidad o estabilidad. Evita convertir toda la aplicación en una simulación artificial.

Limpia el estado compartido

Asegúrate de que cada prueba pueda ejecutarse de forma independiente. Una prueba no debería depender de lo que ocurrió en otra.

Cuida la legibilidad

Un test difícil de leer será difícil de mantener. Usa nombres descriptivos, escenarios claros y expectativas concretas.

Incluye casos negativos

No pruebes solo el camino feliz. Los errores, estados vacíos y validaciones suelen ser donde aparecen muchos bugs reales.

Revisa tus pruebas con el tiempo

Una suite de testing también evoluciona. Lo que hoy tiene sentido puede necesitar ajustes cuando cambia la arquitectura, el producto o el equipo.

Preguntas frecuentes sobre errores al empezar con testing frontend

1. ¿Tengo que probar todos los componentes frontend?

No necesariamente. Probar todos los componentes por obligación puede llevarte a escribir pruebas poco útiles. Es mejor priorizar componentes con lógica, interacción, estados relevantes o impacto directo en la experiencia de usuario.

Un componente puramente visual y muy simple quizá no necesita una prueba específica. En cambio, un formulario con validaciones, un componente de navegación, un modal crítico o una pantalla de checkout sí pueden merecer atención.

2. ¿Qué herramienta debería usar para empezar con testing frontend?

Depende del stack y del tipo de prueba. Para pruebas de componentes en React, Testing Library junto con Jest o Vitest suele ser una opción habitual. Para pruebas end to end, herramientas como Cypress o Playwright son muy utilizadas.

Más importante que la herramienta es el enfoque: escribir pruebas que validen comportamientos relevantes y que puedan mantenerse con el tiempo.

3. ¿Es malo usar mocks en pruebas frontend?

No, usar mocks no es malo. Lo problemático es abusar de ellos o usarlos para evitar probar integraciones importantes. Los mocks son útiles para controlar respuestas, simular errores o evitar dependencias externas, pero deben utilizarse con criterio.

Si una prueba tiene tantos mocks que ya no representa el comportamiento real de la aplicación, quizá ha perdido parte de su valor.

Probar mejor no significa probar más

Empezar con testing frontend no consiste en llenar el proyecto de archivos de prueba ni en perseguir una cobertura perfecta. Consiste en construir confianza.

Una buena suite de pruebas no es la que más impresiona en una métrica, sino la que ayuda al equipo a trabajar con más seguridad. La que detecta errores importantes antes de producción. La que permite refactorizar sin miedo. La que documenta, de forma práctica, cómo debería comportarse la aplicación.

Los errores testing frontend más comunes suelen tener una raíz compartida: olvidarnos de la persona usuaria. Cuando las pruebas se centran demasiado en la implementación, en el porcentaje de cobertura o en detalles internos, pierden parte de su propósito.

En cambio, cuando las pruebas responden a preguntas reales —qué puede hacer una persona, qué ocurre si algo falla, qué mensaje aparece, qué comportamiento debe mantenerse— el testing se vuelve mucho más útil.

La clave está en empezar pequeño, pero empezar bien. No necesitas una estrategia perfecta desde el primer día. Necesitas pruebas claras, mantenibles y orientadas al valor. Con el tiempo, esa base se convierte en una red de seguridad que mejora la calidad del desarrollo frontend y también la tranquilidad del equipo.