Todo el mundo escribe tests. El problema es que muchos se rompen cuando renombras un método y se quedan callados cuando el sistema realmente falla. Un test que no da confianza es solo coste de mantenimiento disfrazado de calidad.

Prueba el comportamiento, no la implementación
Si tu test sabe que el método llama a repository.findById antes que a cache.get, está acoplado al cómo, no al qué. ¿Refactorizaste el orden interno sin cambiar el resultado? El test se rompe. Eso es ruido.
Prueba la interfaz pública y el resultado observable: entrada, salida y el efecto secundario que importa. El detalle interno puede cambiar libremente. Regla práctica: si puedes reescribir la función completa manteniendo el contrato y el test sigue en verde, probaste lo correcto.
Un test, un motivo para fallar
Cuando un test falla, deberías saber qué se rompió con solo leer el nombre. Si valida cinco cosas, el mensaje se convierte en un enigma y abres el debugger. Cada test comprueba un comportamiento.
Eso no significa que deba haber un assert por test: es un concepto por test. Validar que un objeto se creó con el nombre y el correo electrónico correctos son dos asserts del mismo comportamiento; está bien. En cambio, mezclar "crea el usuario" con "envía el correo de bienvenida" son dos tests.
Arrange, Act, Assert (y nada más en medio)
La estructura es sencilla: prepara el escenario, ejecuta la acción y verifica el resultado. Tres bloques, en ese orden, con una línea en blanco entre ellos.
El error habitual es intercalarlos: un assert en medio del arrange, una segunda acción después de la verificación. Entonces el test se convierte en una novela y nadie entiende dónde empieza cada cosa. Si tienes dos "act" en el mismo test, probablemente sean dos tests.
Rápido y determinista o no sirve
Nadie ejecuta una suite lenta. Si el test unitario toca una base de datos, la red o el reloj, ni siquiera es unitario: es una prueba de integración disfrazada, y será lenta e inestable. El objetivo es que cada test tarde milisegundos.
El determinismo no es negociable. Nada de Random, now() ni dependencias del orden de ejecución. Un test que pasa en una ejecución y falla en la siguiente —el famoso flaky— es peor que no tener tests: enseña al equipo a ignorar los resultados rojos. Y cuando se ignora todo rojo, el rojo de verdad pasa desapercibido.
Un nombre que explique qué se rompió
test1, testUser y debeFuncionar no dicen nada. Un buen nombre describe la condición y el resultado esperado: saque_com_saldo_insuficiente_lanca_erro. Cuando este test aparece en rojo en CI, ya sabes cuál es el problema sin abrir el archivo.
Piensa en el nombre como la primera línea del informe de fallo. Se lee muchas más veces de las que se escribe.
Usa mocks con moderación
El mock existe para aislar una dependencia costosa o no determinista, como la pasarela de pagos o el envío de correo. No para crear mocks de todo lo que se mueve.
Cuando mockeas demasiado, el test empieza a verificar que tu código llamó a los métodos que creías que iba a llamar. Eso no es probar el comportamiento; es congelar la implementación actual y volver a dificultar la refactorización. Si un test tiene más líneas de configuración de mocks que de lógica, sospecha: o el diseño está demasiado acoplado o estás probando el mock, no el código.
El test es código de primera clase
La peor mentira que nos contamos es: "es solo un test, no tiene que quedar bonito". El código de test se deteriora igual que el código de producción, solo que nadie lo refactoriza porque "está pasando".
¿El test tiene duplicación? Extráela. ¿El nombre es malo? Cámbialo. ¿La configuración inicial es enorme? Es una señal de mal diseño en el código de producción, no una excusa para ignorarlo. La suite es la documentación ejecutable del sistema: trátala con el mismo respeto.
Un buen test no es el que alcanza una cobertura del 100 %. Es el que te permite cambiar el código sin miedo y grita fuerte cuando rompes algo de verdad. Lo demás es teatro verde.


