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

  1. 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.
  2. Se integra automáticamente al proceso: CI/CD ejecuta los tests sin intervención manual.
  3. Evita regresiones: alerta cuando nuevas features rompen código existente.
  4. 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

  1. 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.
  2. Green: escribe el código mínimo para pasar el test. Promueve simplicidad.
  3. 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):

  1. Difícil de adoptar en equipo: exige cambio de mentalidad.
  2. Desarrollo inicial más lento → gestión reticente (suele compensarse con menos debugging posterior).
  3. 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.