
El mes pasado tuve una conversación reveladora con mi equipo sobre cómo usamos Git.
Asumía que todos trabajaban desde la terminal —especialmente porque gestionamos actualizaciones en más de 14 repositorios—. Pero resultó que yo era el único. Los otros diez desarrolladores usaban exclusivamente la interfaz gráfica de Git en su IDE.
Eso me hizo pensar: ¿cuántas otras prácticas “obvias” no lo son tanto? El testing es una de ellas. Muchos desarrolladores saben que deben escribir tests, pero pocos entienden realmente cómo o por qué —especialmente ahora, cuando la IA está transformando la forma en que escribimos código—. Por eso quería hablar sobre algunas buenas prácticas de testing.
Las herramientas de IA están cambiando las reglas del juego, y el testing es tu red de seguridad
Asistentes como GitHub Copilot o ChatGPT pueden generar código a gran velocidad, pero no sin riesgos. No comprenden realmente los requisitos ni las limitaciones de tu sistema. Pueden producir soluciones que parecen correctas, pero que ignoran casos límite, manejan mal los errores o pasan por alto lógica de negocio importante.
Esto crea riesgos ocultos, especialmente al refactorizar, porque perdemos visibilidad sobre si los cambios introducen errores sutiles.
El testing se convierte así en nuestra principal salvaguarda. Nos obliga a definir de forma explícita qué significa un comportamiento “correcto”, anticipar escenarios de fallo y establecer una red de seguridad que garantice que la integridad del código se mantiene a lo largo de los cambios. En otras palabras: los tests compensan los puntos ciegos de la IA, transformando el código generado en un activo fiable en lugar de un riesgo potencial.
Veamos cómo probar código generado por IA.
El testing marca la diferencia entre “funciona” y “es correcto”
La IA puede automatizar la escritura, pero no el pensamiento. Por eso, las habilidades de testing son hoy más valiosas que nunca. Los equipos que prosperen con IA no serán los que escriban más código, sino los que mejor lo validen.
En la era de la IA, entender por qué el código funciona es tan importante como escribirlo. Entonces, ¿qué hace que un test sea bueno?
Primero, aclaremos qué es un test:
El testing de software es el conjunto de técnicas utilizadas para verificar que el sistema desarrollado cumple con los requisitos establecidos.
Todos probamos nuestro código de alguna forma, aunque sea manualmente. Pero el testing manual es costoso, lento, propenso a errores y difícil de replicar. De ahí la importancia de la automatización.
Los tests automatizados nos permiten ganar velocidad de desarrollo y evitar problemas de mantenimiento graves, siempre que estén bien diseñados. Escribir buenos tests requiere tiempo, pero esa inversión se recupera solo si los tests son de calidad.
Testing funcional vs. no funcional
Podemos clasificar los tests en dos grandes categorías:
Testing funcional: verifica qué hace el sistema. Incluye:
Unit tests (componentes aislados)
Integration tests (interacciones entre componentes)
End-to-End (E2E) tests (simulación de flujos de usuario)
Regression tests (verifican que las funcionalidades existentes siguen funcionando)
Testing no funcional: verifica cómo se comporta el sistema, abarcando aspectos como:
Rendimiento
Seguridad
Accesibilidad
Usabilidad
Mantenibilidad
Normalmente, los desarrolladores se encargan del testing funcional (especialmente unit e integration tests), mientras que los tests no funcionales suelen requerir especialistas en cada área.
La evolución de la Pirámide de Testing
Un principio clásico es la Pirámide de Testing, que propone:
Muchos unit tests (rápidos, fáciles de escribir)
Menos integration tests (más lentos, más complejos de mantener)
Aún menos E2E tests (los más lentos y frágiles)
Aunque este modelo fue propuesto por Mike Cohn en 2009, ha evolucionado. Herramientas modernas como Playwright o Cypress han hecho más prácticos los integration y E2E tests. Además, los analizadores estáticos (como linters o TypeScript) reducen la necesidad de ciertos tests defensivos, permitiéndonos centrarnos en la lógica de negocio.
No existe una estrategia universal; cada proyecto requiere su propio equilibrio. Lo clave es equilibrar velocidad, cobertura y mantenibilidad, adaptándose al stack y las herramientas actuales.
Qué probar
Una de las partes más difíciles del testing es decidir qué probar. No entraré en detalle, pero sí quiero compartir una lección aprendida que cambió por completo mi enfoque.
El error de “probarlo todo”
Cuando llegué a un proyecto sin casi unit tests, la solución parecía evidente: escribir muchos. Y lo hicimos —mockeando agresivamente, buscando un 90 % de cobertura y sintiéndonos satisfechos—. Hasta que llegó la realidad:
Los refactors rompían tests constantemente, aunque el comportamiento no cambiara.
La alta cobertura daba una falsa sensación de seguridad: los bugs seguían apareciendo.
Los mocks excesivos hacían los tests frágiles y desconectados de cómo funcionaba realmente la app.
El problema era claro: estábamos probando detalles de implementación, no el comportamiento que veía el usuario.
Un mejor enfoque
Inspirado en expertos como Kent C. Dodds, cambié mi mentalidad y adopté estos principios:
Prueba como un usuario: céntrate en qué hace el software, no en cómo lo hace.
Olvida las etiquetas rígidas: no importa si un test es “unit” o “integration”, sino si detecta bugs de forma rápida.
Prioriza escenarios significativos: prueba el caso de uso más realista posible, no solo unidades aisladas.
Cobertura ≠ calidad
Por eso desconfío de las métricas de cobertura: a menudo se convierten en un fin en sí mismas. El propósito del testing no es alcanzar un número, sino responder una pregunta esencial:
¿Confiaría en este código en producción?
La clave está en centrarse en el comportamiento, no en la implementación. Así se crean tests que realmente evitan errores, no solo llenan un informe. Si nunca ha trabajado con informes de cobertura, le recomiendo este artículo: Making Use of Code Coverage.
Cómo escribir buenos tests
Escribir tests eficaces es una habilidad que separa a los buenos desarrolladores de los excelentes. Algunos principios básicos:
Rápidos de ejecutar y de escribir
Fiables (no inestables)
Fáciles de leer y entender
Uso adecuado de mocks
Un concepto por test
Código de primera clase (mantenible y con buena estructura)
Los tests deben ser rápidos
Escribimos tests para ahorrar tiempo, no para perderlo. Si los tests tardan demasiado en ejecutarse o en escribirse, dejan de ser útiles.
Cuando los tests son lentos:
Los desarrolladores dejan de ejecutarlos localmente.
Los pipelines de CI se vuelven cuellos de botella.
Un buen test es:
Rápido de ejecutar: se usa constantemente.
Rápido de escribir: se escribe de verdad.
Fácil de entender: mejora la mantenibilidad.
El coste oculto de los tests poco fiables
Erosionan la confianza: si fallan al azar, los desarrolladores los ignoran.
Consumen tiempo: investigar falsos positivos reduce la productividad.
Generan ruido: los errores reales pasan desapercibidos.
A veces, eliminar un test inestable es mejor opción que mantenerlo, especialmente si su coste supera su valor o si prueba una funcionalidad no crítica.
Legibilidad y comprensión
Uno de los errores más comunes al escribir tests automáticos es olvidar que los tests son tan importantes como el código de producción.
DRY vs. legibilidad
Los tests son especificaciones, no solo código. Aunque el principio Don’t Repeat Yourself es clave en el desarrollo, en los tests cierta repetición puede ser positiva si mejora la claridad.

Buen ejemplo: claro y autosuficiente. No utiliza funciones auxiliares externas.
Sin helpers ni configuraciones externas
Fechas y montos concretos que cuentan la historia
La regla de negocio es evidente

Mal ejemplo: usa helpers externos que no se ven directamente.
El nombre del test es vago (¿cuánto se retrasa? ¿qué tarifa?)
La lógica de negocio crítica (más de 30 días) está oculta
El lector debe saltar entre archivos para entenderlo
Longitud del test
Todos estaríamos de acuerdo en que tener una función de 50 líneas no es una buena práctica, pero por alguna razón, cuando se trata de testing, parece aceptable tener un test de 50 líneas.
Un buen test debe ser lo suficientemente corto como para caber completo en la pantalla (normalmente entre 5 y 15 líneas). Si es más largo, probablemente esté haciendo demasiado.

Buen ejemplo: test de solo 3 líneas, centrado en el comportamiento específico que se quiere verificar.

Mal ejemplo: test demasiado largo que prueba varias cosas a la vez —por ejemplo, añadir elementos, eliminarlos y aplicar un descuento.
Un concepto por test
Cada test debe verificar un comportamiento específico (no varios escenarios a la vez).

En estos ejemplos, nos centramos en comprobar una sola cosa en cada test. Por ejemplo, en el primero verificamos que se puedan añadir elementos al carrito de compra y que el precio total se actualice correctamente.
La estructura AAA de los tests: Arrange, Act, Assert
Un test bien estructurado sigue el patrón AAA (Arrange–Act–Assert):
Arrange: prepara el contexto del test. Configura todos los objetos, mocks y datos necesarios. Por ejemplo, si queremos probar un método de una clase, primero debemos instanciar esa clase para poder testearla.
Act: una vez preparado el contexto, ejecutamos la acción que queremos probar. Por ejemplo, invocar un método con determinados parámetros.
Assert: verificamos que el resultado de la acción sea el esperado. Por ejemplo, comprobar que el método anterior devuelve un valor concreto.
Incluye solo lo necesario para el test en cuestión. Los datos concretos mostrados en un test deben ser relevantes para diferenciarlo del resto. La información irrelevante para el comportamiento que queremos comprobar debería ocultarse. De esta forma, cuando vemos cadenas, números u otros detalles literales, sabemos que son significativos y merecen atención.

Buen ejemplo: Solo se crea un usuario con la propiedad age porque queremos comprobar que la función isAdult
devuelve true
cuando la edad es 18.

Mal ejemplo: Queremos comprobar que isAdult
devuelve true
cuando la edad es mayor de 18, pero en este test se añaden más propiedades al objeto user que no son necesarias para esta comprobación.
Descripción del test
Dedicamos mucho tiempo a pensar en los nombres de las variables, pero cuando se trata de un test, a menudo creemos que nombrarlo correctamente no es tan importante. Sin embargo, explicar qué estás probando es tan importante como el test en sí.
Un test puede actuar como documentación de tu código —una documentación viva, ya que estás obligado a actualizarlo cada vez que cambia la lógica de negocio—. Al nombrar bien un test, también estás aclarando el problema y la solución; por eso, invertir tiempo en explicar lo que hace el test es una buena práctica.
Los nombres de los tests deben ser enunciados claros, escritos en lenguaje de negocio, que describan el comportamiento del sistema. Si alguien puede entender cómo se comporta el sistema solo leyendo los nombres de los tests, significa que están correctamente definidos.
El contenido del test representa un ejemplo concreto de un escenario, una instantánea del comportamiento del sistema en un momento determinado y con ciertos valores. Por tanto, el nombre del test no debe incluir datos concretos, sino la regla de negocio general que se está demostrando.

Buen ejemplo de test (Shopping Cart): test cart with 2 items and SUMMER20 coupon should return 20% discount.

Buen ejemplo de test (Shopping Cart): applies 15% discount for premium users.
Uso eficaz de los mocks
Los test doubles (objetos simulados) son un mal necesario. Cuanto menos mockeas, más se parecerá tu test al entorno real. Sin embargo, mockear demasiado reduce la calidad del test; no mockear lo suficiente lo hace más lento.
Al igual que decidir qué probar, saber qué mockear no es tarea fácil. Dos cosas me han ayudado a mejorar: conocer los tipos de mocks y entender los errores más comunes.
Tipos de mocks
Más que los nombres, lo importante es entender las diferencias entre ellos, ya que eso marca una gran diferencia en la práctica.
Dummy: es un objeto o valor de relleno usado solo para cumplir con los parámetros de un test, pero que no participa en la lógica de prueba. Se utiliza cuando necesitas pasar un objeto a un método, pero su comportamiento no es relevante para el caso que estás probando.
Stub: proporciona respuestas predefinidas a llamadas de método. Es útil cuando el elemento que estás probando depende no solo de sus argumentos, sino también de una fuente externa de datos. El stub permite reemplazar esa fuente externa.
Mock y Spy: verifican interacciones (por ejemplo, si un método fue llamado con ciertos argumentos). La diferencia es que el spy delega al objeto real y mantiene su comportamiento original.
Fake: es una implementación funcional ligera que sustituye una dependencia real (por ejemplo, para ganar velocidad o simplicidad). Un buen ejemplo sería una base de datos en memoria usada para testing.
Errores comunes
Mockear en exceso. Simular todas las dependencias hace que los tests se acoplen demasiado a los detalles de implementación y se rompan con facilidad al refactorizar.
Verificar detalles internos con mocks o spies. Normalmente deberíamos probar resultados, no pasos internos.
Mockear lo que no te pertenece. Simular librerías o código del framework (por ejemplo,
axios
,fetch
) puede hacer que los mocks se desvíen del comportamiento real, ocultando errores de integración.
Haz del testing una prioridad
Nunca debemos tratar los tests como algo menos importante que el código. Una práctica sencilla que me ha ayudado es no separar test e implementación: una tarea no está terminada hasta que los tests están implementados, pasan en verde y han sido refactorizados.
Nunca permitas que tu equipo diga:
“No tenemos tiempo para los tests ahora. Entreguemos el código y los añadimos después.”
A medida que avanzamos en esta era del desarrollo impulsado por IA, la supervisión y la validación del código son más cruciales que nunca.
Ariadna Gomez Ruiz
Desarrollo interfaces sólidas y fáciles de usar, con código limpio y buenas pruebas. Me apasionan el rendimiento, la accesibilidad y crear soluciones mantenibles. Vamos a construir algo increíble, componente a componente.