Un mock es un doble de prueba configurable que permite observar cómo se usa una dependencia.
En el artículo anterior probamos una calculadora simple. Ahora vamos a trabajar con servicios que llaman a repositorios, APIs externas y sistemas de archivos.
El problema es que un Test Unitario debe ser aislado.
- Si tu test intenta conectar a SQL Server, ya no es unitario (es de integración).
- Si tu test falla porque se ha caído el WiFi, es un mal test.
- Si tu test tarda 2 segundos en ejecutarse porque hace una query lenta, nadie querrá ejecutar los tests.
¿Cómo probamos la lógica de un servicio (UsuarioService) sin ejecutar realmente la base de datos (UsuarioRepository)?
La respuesta es usar dobles de prueba. Un stub devuelve respuestas preparadas; un mock, además, permite verificar interacciones. Bibliotecas como Moq permiten hacer ambas cosas con el mismo objeto.
Qué es un mock
Un Mock es un objeto simulado que imita el comportamiento de una interfaz real de forma controlada.
En una película de acción, el protagonista no salta del edificio; lo hace un doble.
- Interfaz:
ISaltarEdificios(El contrato: “hay que saltar”). - Implementación Real: El actor famoso (Caro, delicado, si se rompe paramos el rodaje).
- Mock: el doble de prueba. Hace lo que le digas para esa prueba concreta.
En nuestro código:
- No inyectamos el
SqlUsuarioRepositoryreal. - Inyectamos un
Mock<IUsuarioRepository>al que le decimos: “Cuando te pidan el usuario 1, devuelve esto. No preguntes, solo hazlo”.
Moq y NSubstitute
En .NET existen dos grandes librerías para esto:
- Moq: una de las más conocidas. Sintaxis algo más explícita, pero muy potente.
- NSubstitute: una alternativa con sintaxis más fluida y legible.
En este artículo usaremos Moq porque es muy habitual en proyectos .NET, pero la teoría aplica a ambas.
Instalación
dotnet add package MoqEl escenario que vamos a probar
Tenemos un servicio que busca un usuario en el repositorio. Si no existe, lanza un error. Si existe, devuelve su nombre en mayúsculas.
// La Interfaz (La dependencia)
public interface IUsuarioRepository
{
Usuario? GetById(int id);
}
// La Clase a probar (SUT: System Under Test)
public class UsuarioService
{
private readonly IUsuarioRepository _repo;
public UsuarioService(IUsuarioRepository repo)
{
_repo = repo;
}
public string ObtenerNombreMayusculas(int id)
{
var usuario = _repo.GetById(id); // 👈 Dependencia externa
if (usuario == null) throw new KeyNotFoundException("Usuario no existe");
return usuario.Nombre.ToUpper();
}
}Crear la prueba con Moq
Vamos a probar el “camino feliz”: El repositorio encuentra al usuario.
using Moq;
using Xunit;
public class UsuarioServiceTests
{
[Fact]
public void ObtenerNombre_SiUsuarioExiste_DevuelveNombreEnMayusculas()
{
// 1. ARRANGE (Preparar el escenario)
// Creamos el mock de la interfaz
var mockRepo = new Mock<IUsuarioRepository>();
// CONFIGURACIÓN (Setup): Le damos el guion al mock
// "Cuando alguien llame a GetById(1), devuelve un usuario llamado 'Luis'"
mockRepo.Setup(repo => repo.GetById(1))
.Returns(new Usuario { Id = 1, Nombre = "Luis" });
// Inyectamos el OBJETO simulado (.Object) en nuestro servicio
var servicio = new UsuarioService(mockRepo.Object);
// 2. ACT (Acción)
var resultado = servicio.ObtenerNombreMayusculas(1);
// 3. ASSERT (Verificación)
Assert.Equal("LUIS", resultado);
}
}Fíjate en el mecanismo.
No hemos necesitado base de datos. UsuarioService cree que ha hablado con el repositorio, pero en realidad ha hablado con nuestro objeto mentiroso que devolvió “Luis” instantáneamente.
Probar errores y argumentos genéricos
¿Qué pasa si queremos probar que lanza excepción cuando el usuario no existe? Y además, ¿qué pasa si queremos que el Mock responda a cualquier ID, no solo al 1?
Usamos It.IsAny<T>().
[Fact]
public void ObtenerNombre_SiUsuarioNoExiste_LanzaExcepcion()
{
// Arrange
var mockRepo = new Mock<IUsuarioRepository>();
// Configuración: "Llama con el entero que quieras, yo te devuelvo null"
mockRepo.Setup(x => x.GetById(It.IsAny<int>()))
.Returns((Usuario?)null);
var servicio = new UsuarioService(mockRepo.Object);
// Act & Assert
Assert.Throws<KeyNotFoundException>(() =>
{
servicio.ObtenerNombreMayusculas(999);
});
}Verificar comportamiento con Verify
A veces no nos importa lo que devuelve el método, sino si se ha llamado a un método.
Ejemplo: “Si borro un usuario, asegúrate de que se llamó al método Delete del repositorio”.
[Fact]
public void BorrarUsuario_LlamaAlRepositorioCorrectamente()
{
// Arrange
var mockRepo = new Mock<IUsuarioRepository>();
var servicio = new UsuarioService(mockRepo.Object);
// Act
servicio.Borrar(5);
// Assert: Verificamos que el método Delete(5) fue llamado EXACTAMENTE una vez
mockRepo.Verify(repo => repo.Delete(5), Times.Once);
}Esto es potentísimo para probar efectos secundarios (envío de emails, escritura en logs, etc.).
La alternativa: NSubstitute
Solo para que veas la diferencia de sintaxis. NSubstitute intenta leerse más como lenguaje natural, evitando los .Setup() y .Object.
// Con NSubstitute
var repo = Substitute.For<IUsuarioRepository>();
// Configuración
repo.GetById(1).Returns(new Usuario { Nombre = "Luis" });
// Uso
var servicio = new UsuarioService(repo); // No hace falta .ObjectEs cuestión de gustos. Moq es más explícito (“Setup”), NSubstitute es más automático.
Errores comunes al crear mocks
- Simular clases concretas: Moq puede trabajar con interfaces y con miembros
virtualde clases no selladas. Las interfaces suelen dejar el contrato más claro, pero no hay que crearlas solo para satisfacer a la biblioteca de mocks. - Mockear el DbContext: intentar mockear
DbContextde Entity Framework suele ser frágil. Para lógica de negocio, mockea una interfaz propia. Para comprobar consultas reales, usa tests de integración con SQLite en memoria, una base de datos desechable o Testcontainers. - Mockear DTOs: No mockees objetos de datos (Usuario, Producto). Simplemente haz
new Usuario(). Solo mockea comportamiento (Servicios).