Unit Testing y Mocking en .NET 8: la guía que evita que tus tests sean inútiles
El unit testing es fácil de hacer mal y volverse inútil, socavando la confianza del equipo. Hacerlo bien ahorra tiempo y dinero: tipos de testing, resultados de un test, atributos de los buenos tests, test doubles (mocks vs stubs), TDD con Red-Green-Refactor, las nueve reglas FIRSTHAND y mocking práctico con NSubstitute.
1. No todo es unit testing
| Tipo | Qué verifica |
|---|---|
| Unit | Un método aislado de sus dependencias (con mocks) |
| Integración / E2E / Seguridad | Que componentes funcionen juntos: BD, APIs, capas |
| Rendimiento / Carga / Estrés | Comportamiento bajo carga normal o extrema |
| Funcionales / Usabilidad | Cumplimiento de requisitos y experiencia de usuario |
La frontera clave: un test de integración puede parecer unit test (usa las mismas herramientas, como xUnit), pero toca la BD real, tarda más o no está aislado. Si tu test depende de infraestructura, ya no es un unit test.
2. Los cuatro resultados posibles de un test
Dos ejes: positivo/negativo (¿detecta presencia o ausencia de defecto?) y verdadero/falso (¿refleja la realidad del código?).
- TP: encuentra un defecto que existe ✅
- TN: confirma que no hay defecto ✅
- FP: reporta un defecto inexistente ❌ (erosiona la confianza en la suite)
- FN: pasa por alto un defecto real ❌ (el peor escenario)
3. Atributos de todos los buenos tests
- Verifica lo más importante del codebase: modelo de dominio y lógica de negocio. La cobertura no necesita ser 100%: es métrica para detectar zonas sin probar, no objetivo. Verifica el resultado final, no los detalles técnicos de la implementación.
- Se integra automáticamente al proceso: CI/CD ejecuta los tests sin intervención manual.
- Evita regresiones: alerta cuando nuevas features rompen código existente.
- Rápidos, fiables, independientes y legibles: si tardan minutos, nadie los ejecutará; no deben fallar según el orden de ejecución ni el día de la semana.
4. Test doubles: mocks vs. stubs (y CQS)
Un test double reemplaza una dependencia real difícil de configurar de forma consistente:
- Mocks → simulan interacciones salientes: tu código envía algo hacia afuera (ej. enviar email al registrar). Pueden cambiar estado externo. Se verifican con
Received(). - Stubs → simulan interacciones entrantes: tu código recibe datos (ej. leer producto desde BD). Devuelven respuestas fijas con
Returns().
La separación conecta con Command-Query Separation (CQS): todo método debe ser comando o consulta.
Mocks son para comandos (pueden tener efectos secundarios, no devuelven valor); stubs para consultas (sin efectos secundarios, devuelven valor).
Regla práctica: usa directamente los sistemas externos bajo tu control; haz mock de los que no controlas.
5. TDD: Red-Green-Refactor con honestidad
- Red: escribe el test primero; falla porque la funcionalidad no existe aún (la implementación lanza
NotImplementedException). Esto garantiza que tus tests son significativos. - Green: escribe el código mínimo para pasar el test. Promueve simplicidad.
- Refactor: limpia el código integrándolo bien con el codebase. Tras refactorizar, todos los tests deben seguir en verde.
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator(); // Arrange
var result = calculator.Add(2, 3); // Act
Assert.Equal(5, result); // Assert
}
Pros: diseño y requisitos pensados por adelantado → software mejor diseñado, fiable y mantenible.
Contras (los admite abiertamente el libro):
- Difícil de adoptar en equipo: exige cambio de mentalidad.
- Desarrollo inicial más lento → gestión reticente (suele compensarse con menos debugging posterior).
- Sobrecarga de mantenimiento: la suite evoluciona con la aplicación.
Buenas prácticas: empieza con tests simples, enfócate en uno a la vez, refactoriza código y tests regularmente, y combina TDD con pair programming y code reviews.
6. Las nueve reglas FIRSTHAND
Adam Tibi (Pragmatic Test-Driven Development in C# and .NET) condensa sus mejores prácticas en el acrónimo FIRSTHAND, que complementa al ciclo Red-Green-Refactor:
| Letra | Regla | Idea clave |
|---|---|---|
| F | First | Escribe el test antes que el código |
| I | Intention | El nombre del test revela qué comportamiento verifica |
| R | Readability | Estructura AAA clara; el test es documentación viva |
| S | Single-Behavior | Un test = un comportamiento; si falla, sabes qué se rompió |
| T | Thoroughness | Suficientes tests; nunca llames dependencias reales de terceros |
| H | High-Performance | Rápidos para ejecutarse siempre |
| A | Automation | Integrados al pipeline de CI |
| N | No Interdependency | Independientes entre sí y del orden de ejecución |
| D | Deterministic | Mismo input → mismo resultado, cero flakiness |
Si revisas tu suite contra esta tabla y algún test falla en una fila, ya tienes trabajo de refactorización identificado.
7. Mocking práctico con NSubstitute
Se instala con dotnet add package NSubstitute. Su API es minimalista:
ICalculator calc = Substitute.For<ICalculator>(); // double de una interfaz
var s = Substitute.For<ClassWithConstructor>(5, "Error"); // clase con constructor
repo.GetProductName(1).Returns("Teclado"); // stub: dato entrante
emailSender.Received().SendEmail(to, subj, body); // mock: verificar salida
Ejemplo completo con AAA (stub):
[Fact]
public void Build_ExistingId_ReturnsFormattedName()
{
// Arrange
var repo = Substitute.For<IProductRepository>();
repo.GetProductName(1).Returns("Teclado");
var sut = new PriceTag(repo);
// Act
var result = sut.Build(1);
// Assert
Assert.Equal("Producto: Teclado", result);
}
Para un comando (mock) el patrón es igual, pero el Assert verifica una interacción: tras sut.Register(email), afirma emailSender.Received().SendEmail(email, "Bienvenido", ...). Ahí está la diferencia entre probar consultas y comandos que mencionaba CQS.
Conclusión
Ubica cada tipo de test en su nivel, interpreta sus resultados con honestidad (teme a los FP/FN), exige a cada test los atributos mínimos, usa el double correcto según CQS, adopta TDD conociendo sus costes reales y audita tu suite contra FIRSTHAND. El unit testing bien hecho ahorra tiempo y dinero; mal hecho, solo añade ruido que erosiona la confianza del equipo.
Fuentes: Mark J. Price, Tools and Skills for .NET 8, cap. 11 (Packt); Adam Tibi, Pragmatic Test-Driven Development in C# and .NET, cap. 6 (Packt); Mark J. Price, Real-World Web Development with .NET 9, cap. 12; Matt Eland, Refactoring with C#, caps. 6–7.