Uso de frameworks de mock en C# (Moq, NSubstitute)
Aprende a usar frameworks de mock como Moq y NSubstitute en C# para aislar dependencias en pruebas unitarias.
En las pruebas unitarias reales, es esencial aislar el comportamiento de la clase que se está probando de sus dependencias.
Aquí es donde entran en juego los frameworks de mock.
Bibliotecas populares como Moq y NSubstitute crean objetos simulados (mock objects)
que permiten emular las dependencias.
De esta manera, solo se verifica el comportamiento de la clase bajo prueba.
¿Qué es un Mock?
Un mock es un objeto simulado que reemplaza las dependencias reales de una clase. Durante las pruebas, esto permite:
- No depender de conexiones reales a bases de datos o servicios,
- Ejecutar las pruebas de manera más rápida y aislada,
- Evitar errores causados por factores externos (API, red, disco),
- Verificar si ciertos métodos fueron realmente llamados.
// Escenario de ejemplo: servicio de registro de usuarios que depende de un servicio de correo electrónico
public interface IEmailServicio
{
void Enviar(string direccion, string mensaje);
}
public class RegistroUsuario
{
private readonly IEmailServicio _emailServicio;
public RegistroUsuario(IEmailServicio emailServicio)
{
_emailServicio = emailServicio;
}
public void Registrar(string nombre)
{
// Proceso de registro...
_emailServicio.Enviar("admin@site.com", $"{nombre} ha sido registrado con éxito!");
}
}
Al probar esta clase, no queremos enviar un correo real. En su lugar, utilizamos un servicio de correo electrónico simulado.
¿Qué es el framework Moq?
Moq es uno de los frameworks de mock más populares del ecosistema .NET. Utiliza una API fluida (Fluent API), es fácil de leer y ofrece un potente soporte de verificación.
// Instalación mediante NuGet:
dotnet add package Moq
using Moq;
using Xunit;
public class RegistroUsuarioTests
{
[Fact]
public void Registrar_DebeEnviarCorreo()
{
// Arrange
var mockCorreo = new Mock<IEmailServicio>();
var registro = new RegistroUsuario(mockCorreo.Object);
// Act
registro.Registrar("Ahmet");
// Assert
mockCorreo.Verify(
m => m.Enviar("admin@site.com", "Ahmet ha sido registrado con éxito!"),
Times.Once); // El método debe llamarse una vez
}
}
El método Verify() comprueba si un comportamiento específico realmente ocurrió.
Esto permite realizar pruebas basadas en comportamiento (behavior verification).
Definir el comportamiento de los Mocks
Con Moq no solo puedes verificar comportamientos, sino también definir los valores de retorno de los métodos simulados.
public interface IStockServicio
{
int Obtener(string codigoProducto);
}
public class VentaProducto
{
private readonly IStockServicio _stock;
public VentaProducto(IStockServicio stock) => _stock = stock;
public bool Vender(string codigoProducto)
{
int cantidad = _stock.Obtener(codigoProducto);
return cantidad > 0;
}
}
public class VentaProductoTests
{
[Fact]
public void SiHayStock_VentaDebeSerExitosa()
{
var mock = new Mock<IStockServicio>();
mock.Setup(s => s.Obtener("ABC123")).Returns(10);
var venta = new VentaProducto(mock.Object);
bool resultado = venta.Vender("ABC123");
Assert.True(resultado);
}
}
El método Setup() define cómo debe comportarse el objeto simulado en determinadas llamadas.
Con Returns() se especifica el valor que debe devolver.
Uso de Callback
En Moq, el método Callback() se utiliza para agregar un efecto secundario (side effect) cuando se llama a un método —
por ejemplo, registrar un log o incrementar un contador durante la prueba.
int contador = 0;
var mock = new Mock<IEmailServicio>();
mock.Setup(m => m.Enviar(It.IsAny<string>(), It.IsAny<string>()))
.Callback(() => contador++);
var registro = new RegistroUsuario(mock.Object);
registro.Registrar("Zeynep");
Assert.Equal(1, contador);
En este ejemplo, el contador aumenta cada vez que se llama a Enviar().
Este enfoque es muy útil para rastrear eventos durante las pruebas.
La clase auxiliar It
Moq proporciona la clase It para realizar comprobaciones de parámetros. Esto permite probar solo ciertos parámetros o usar comodines.
It.IsAny<T>()– Acepta cualquier valor.It.Is<T>(predicate)– Coincide con valores que cumplen una condición.It.IsInRange(min, max, Range.Inclusive)– Verifica rangos de valores.
mock.Verify(m =>
m.Enviar(It.IsAny<string>(),
It.Is<string>(msg => msg.Contains("Ahmet"))),
Times.Once);
¿Qué es el Framework NSubstitute?
NSubstitute ofrece una API sencilla para crear mocks, similar a Moq. Destaca por su alta legibilidad y soporta de forma natural el patrón “Arrange–Act–Assert”.
// Instalación a través de NuGet:
dotnet add package NSubstitute
using NSubstitute;
using Xunit;
public class PruebasRegistroUsuario
{
[Fact]
public void Registrar_DebeEnviarCorreo()
{
var email = Substitute.For<IServicioEmail>();
var registro = new RegistroUsuario(email);
registro.Registrar("Mehmet");
email.Received(1).Enviar("admin@site.com", "¡Mehmet se ha registrado correctamente!");
}
}
El método Received() verifica si una llamada al método realmente ocurrió.
Proporciona una sintaxis más natural y legible para las pruebas.
Definición de Comportamientos (Returns y When)
En NSubstitute, los comportamientos de los mocks pueden definirse con los métodos Returns() y When().
var stock = Substitute.For<IServicioStock>();
stock.Obtener("P001").Returns(5);
var venta = new VentaProducto(stock);
bool resultado = venta.Vender("P001");
Assert.True(resultado);
// Agregar un efecto secundario durante la llamada:
stock.When(s => s.Obtener("P001"))
.Do(_ => Console.WriteLine("El servicio de stock fue llamado."));
La estructura When(...).Do(...) es ideal para definir acciones personalizadas cuando ocurre una llamada específica.
Comparación: Moq vs NSubstitute
| Característica | Moq | NSubstitute |
|---|---|---|
| Estilo de uso | Fluent API (Setup/Verify) | Lenguaje natural (Returns/Received) |
| Legibilidad | Alta | Muy alta |
| Rendimiento | Un poco más rápido | Promedio |
| Soporte de Callback | Sí (.Callback()) | Sí (.Do()) |
| Popularidad | Muy común (frecuente con xUnit) | Menos común, pero sintaxis moderna |
Ejemplo Real: Prueba del Servicio de Pedidos
El siguiente ejemplo muestra un escenario donde se prueba una dependencia de servicio usando Moq y NSubstitute.
public interface IServicioPedidos
{
void Guardar(string producto);
}
public class ServicioCorreo
{
public virtual void Enviar(string mensaje)
{
Console.WriteLine($"Correo enviado: {mensaje}");
}
}
public class ProcesadorPedidos
{
private readonly IServicioPedidos _pedidos;
private readonly ServicioCorreo _correo;
public ProcesadorPedidos(IServicioPedidos pedidos, ServicioCorreo correo)
{
_pedidos = pedidos;
_correo = correo;
}
public void CompletarPedido(string producto)
{
_pedidos.Guardar(producto);
_correo.Enviar($"{producto} pedido completado!");
}
}
// Prueba con Moq
var pedidosMock = new Mock<IServicioPedidos>();
var correoMock = new Mock<ServicioCorreo>();
var proceso = new ProcesadorPedidos(pedidosMock.Object, correoMock.Object);
proceso.CompletarPedido("Laptop");
pedidosMock.Verify(p => p.Guardar("Laptop"), Times.Once);
correoMock.Verify(c => c.Enviar("Laptop pedido completado!"), Times.Once);
// Prueba con NSubstitute
var pedidos = Substitute.For<IServicioPedidos>();
var correo = Substitute.For<ServicioCorreo>();
var proceso2 = new ProcesadorPedidos(pedidos, correo);
proceso2.CompletarPedido("Laptop");
pedidos.Received(1).Guardar("Laptop");
correo.Received(1).Enviar("Laptop pedido completado!");
Rendimiento y Mejores Prácticas
- Use objetos mock solo para dependencias externas (base de datos, servicio, API, acceso a archivos, etc.).
- Cuando pruebe la lógica de negocio, use clases reales; los mocks deben representar solo servicios externos.
- Evite verificaciones innecesarias: las pruebas deben ser simples y claras.
- Siga el patrón
Arrange – Act – Asserten las pruebas unitarias. - Identifique cuándo usar Fakes o Stubs en lugar de mocks.
TL;DR
- Frameworks de Mock crean objetos falsos para aislar dependencias en las pruebas.
- Moq: utiliza una API fluida, ampliamente adoptado y con sólida verificación.
- NSubstitute: ofrece una sintaxis más natural y legible.
Setup()yReturns()→ Definen el comportamiento.Verify()/Received()→ Verifican las llamadas.- Los mocks se centran en probar la clase objetivo sin interactuar con sistemas reales.
Artículos relacionados
Delegados y Eventos en C#
Aprende delegados y eventos en C# para crear aplicaciones basadas en eventos con callbacks y ejemplos prácticos.
Escribir pruebas unitarias en C# (xUnit, NUnit, MSTest)
Aprende a escribir pruebas unitarias en C# con xUnit, NUnit y MSTest para crear aplicaciones más confiables.
Fundamentos de Inyección de Dependencias en C#
Aprende los fundamentos de Inyección de Dependencias en C#, gestionando dependencias y logrando bajo acoplamiento.