aspnet-core-unit-testing-xunit-introduccion

Pruebas unitarias en .NET con xUnit

  • 4 min

Una prueba unitaria comprueba una unidad pequeña de comportamiento de forma aislada y repetible.

Consiste en probar la pieza de código más pequeña posible (generalmente un método) de forma totalmente aislada.

  • Sin base de datos.
  • Sin red.
  • Sin llamar a otras APIs.

Si la prueba falla, debería señalar una unidad de comportamiento concreta. Si depende de que la base de datos esté encendida, ya estamos hablando de una prueba de integración (que veremos más adelante).

En el ecosistema .NET, una de las herramientas más usadas para pruebas es xUnit.

Configuración del proyecto de pruebas

Los tests no viven en tu proyecto principal (MiApi). Viven en un proyecto aparte, generalmente una librería de clases.

La estructura típica de una solución profesional es:

📂 MiSolucion
 ├── 📂 src
 │    └── 📜 MiApi
 └── 📂 tests
      └── 📜 MiApi.UnitTests (Proyecto xUnit)
Copied!

Para crear esto desde la terminal:

# 1. Crear el proyecto de tests
dotnet new xunit -n MiApi.UnitTests

# 2. Añadir referencia al proyecto que queremos probar
cd MiApi.UnitTests
dotnet add reference ../../src/MiApi/MiApi.csproj
Copied!

Anatomía de una prueba: el patrón AAA

Vamos a imaginar que tenemos un servicio simple con lógica de negocio pura.

// En MiApi (src)
public class CalculadoraDescuentos
{
    public decimal Calcular(decimal precio, bool esVip)
    {
        if (precio < 0) throw new ArgumentException("El precio no puede ser negativo");

        if (esVip) return precio * 0.90m; // 10% descuento

        return precio;
    }
}
Copied!

Para probar esto, creamos una clase en nuestro proyecto de tests. Un test bien escrito sigue el patrón AAA:

Arrange (Preparar): Instancias la clase y preparas los datos.

Act (Actuar): Ejecutas el método que quieres probar.

Assert (Afirmar): Verificas que el resultado es el esperado.

using Xunit; // La librería principal

public class CalculadoraDescuentosTests
{
    [Fact] // 👈 Esto marca el método como un Test
    public void Calcular_SiUsuarioEsVip_Aplica10PorcientoDescuento()
    {
        // 1. Arrange
        var calculadora = new CalculadoraDescuentos();
        decimal precioOriginal = 100m;

        // 2. Act
        decimal resultado = calculadora.Calcular(precioOriginal, esVip: true);

        // 3. Assert
        Assert.Equal(90m, resultado); // Esperamos 90
    }
}
Copied!

Ejecutar las pruebas

Puedes usar el explorador de pruebas de Visual Studio o ejecutar en la terminal: dotnet test

Fact y Theory

xUnit tiene dos atributos principales:

  • [Fact]: una prueba sin parámetros que representa un único caso.
  • [Theory]: una prueba parametrizada que se ejecuta varias veces con datos distintos.

Queremos probar que el descuento no se aplica a usuarios normales. ¿Probamos con 100? ¿Y con 50? ¿Y con 0?

[Theory]
[InlineData(100, 100)] // Si entra 100, sale 100
[InlineData(50, 50)]   // Si entra 50, sale 50
[InlineData(0, 0)]     // Si entra 0, sale 0
public void Calcular_SiNoEsVip_NoAplicaDescuento(decimal precio, decimal esperado)
{
    // Arrange
    var calculadora = new CalculadoraDescuentos();

    // Act
    var resultado = calculadora.Calcular(precio, esVip: false);

    // Assert
    Assert.Equal(esperado, resultado);
}
Copied!

Con [Theory] y [InlineData], hemos escrito 3 tests en el espacio de uno.

Probar excepciones

Un buen test no solo prueba el “camino feliz”, también prueba que el código falle cuando debe fallar. Nuestra calculadora lanza una excepción si el precio es negativo. ¿Cómo probamos eso?

[Fact]
public void Calcular_SiPrecioNegativo_LanzaExcepcion()
{
    // Arrange
    var calculadora = new CalculadoraDescuentos();

    // Act & Assert
    // Capturamos la excepción
    Assert.Throws<ArgumentException>(() =>
    {
        calculadora.Calcular(-10, true);
    });
}
Copied!

Mejorar la legibilidad

Los Assert.Equal(esperado, actual) de xUnit funcionan, pero a veces son confusos (¿cuál era el esperado, el primero o el segundo?).

Durante años, FluentAssertions ha sido una librería muy popular para hacer que los tests se lean como lenguaje natural.

Desde la versión 8, FluentAssertions cambió su licencia y el uso comercial requiere revisar sus condiciones. En proyectos de empresa, valora usar Assert de xUnit, fijar una versión compatible con tu licencia o usar alternativas como Shouldly.

dotnet add package FluentAssertions
Copied!

Mira la diferencia:

using FluentAssertions;

// xUnit clásico
Assert.Equal(90m, resultado);
Assert.StartsWith("El precio", excepcion.Message);

// FluentAssertions
resultado.Should().Be(90m);
excepcion.Message.Should().StartWith("El precio");
lista.Should().NotBeEmpty().And.HaveCount(3);
Copied!

El problema de las dependencias

Hasta aquí todo ha sido fácil porque CalculadoraDescuentos no dependía de nadie. Era lógica pura.

En una aplicación real, los servicios se parecen más a esto:

public class ProductoService
{
    private readonly IProductoRepository _repo; // Dependencia

    public ProductoService(IProductoRepository repo)
    {
        _repo = repo;
    }

    public Producto Get(int id)
    {
        var producto = _repo.GetById(id); // Llama a BBDD
        if (producto == null) throw new KeyNotFoundException();
        return producto;
    }
}
Copied!

Si intentas probar esto haciendo new ProductoService(...), el compilador te pedirá un repositorio. Y no puedes pasarle el repositorio real porque:

  1. El test unitario no debe tocar la base de datos.
  2. Sería lentísimo.

En este punto aparecen los Mocks (simulacros). Necesitamos crear un “repositorio de mentira” que se comporte como nosotros queramos para engañar al servicio.