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)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.csprojAnatomí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;
}
}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
}
}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);
}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);
});
}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 FluentAssertionsMira 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);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;
}
}Si intentas probar esto haciendo new ProductoService(...), el compilador te pedirá un repositorio. Y no puedes pasarle el repositorio real porque:
- El test unitario no debe tocar la base de datos.
- 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.