Cargando...

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:


// 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.


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ísticaMoqNSubstitute
Estilo de usoFluent API (Setup/Verify)Lenguaje natural (Returns/Received)
LegibilidadAltaMuy alta
RendimientoUn poco más rápidoPromedio
Soporte de CallbackSí (.Callback())Sí (.Do())
PopularidadMuy 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


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() y Returns() → 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