Что такое Moq и зачем он нужен?
Moq (произносится как "Мок") — это популярная библиотека для создания mock-объектов (заглушек) в C# (.NET). Она является частью экосистемы модульного тестирования и используется для изоляции тестируемого кода от его зависимостей.
В реальных приложениях классы часто зависят от других классов: базы данных, веб-сервисов, файловой системы, API сторонних сервисов. При написании юнит-тестов мы не хотим тестировать эти внешние зависимости — мы хотим проверить только логику нашего класса. Moq позволяет создавать "фальшивые" (mock) объекты, которые имитируют поведение реальных зависимостей, но при этом полностью контролируются тестом.
Основные сценарии использования Moq:
- Изоляция тестов: тестирование одного класса без запуска базы данных или внешних сервисов.
- Проверка взаимодействий: убедиться, что метод был вызван с правильными аргументами, определенное количество раз.
- Задание возвращаемых значений: заставить зависимость возвращать нужные данные для проверки логики.
- Симуляция исключений: проверить, как код обрабатывает ошибки от внешних систем.
Moq работает с интерфейсами и виртуальными методами классов. Это ключевое ограничение: вы не можете замокать статический метод или невиртуальный метод.
Установка Moq
Moq распространяется через NuGet — менеджер пакетов для .NET. Установить его можно несколькими способами:
Через .NET CLI (рекомендуется)
dotnet add package Moq
Через Package Manager Console (Visual Studio)
Install-Package Moq
Через NuGet Package Manager UI
В Visual Studio: ПКМ на проекте → Manage NuGet Packages → Поиск "Moq" → Установить
Требования: .NET Framework 4.5+ или .NET Core 2.0+ / .NET 5+
Быстрый старт: первый тест с Moq
Предположим, у нас есть интерфейс репозитория и сервис, который его использует:
public interface IUserRepository
{
User GetUserById(int id);
void SaveUser(User user);
}
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
public string GetUserName(int userId)
{
var user = _repository.GetUserById(userId);
return user?.Name ?? "Unknown";
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
Теперь напишем тест с использованием Moq:
using Moq;
using Xunit;
public class UserServiceTests
{
[Fact]
public void GetUserName_WhenUserExists_ReturnsUserName()
{
// Arrange
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(repo => repo.GetUserById(1))
.Returns(new User { Id = 1, Name = "Alice" });
var service = new UserService(mockRepo.Object);
// Act
var result = service.GetUserName(1);
// Assert
Assert.Equal("Alice", result);
mockRepo.Verify(repo => repo.GetUserById(1), Times.Once);
}
}
Разбор примера:
new Mock<IUserRepository>()— создаем mock-объект для интерфейса..Setup(...)— настраиваем поведение: при вызовеGetUserById(1)вернуть пользователя Alice.mockRepo.Object— получаем "фальшивый" объект, который передаем в сервис.mockRepo.Verify(...)— проверяем, что метод был вызван ровно один раз.
Основные методы и функции Moq
1. Mock<T>.Setup()
Что делает: Настраивает поведение метода или свойства mock-объекта. Определяет, что должно произойти при вызове определенного члена.
Параметры: Лямбда-выражение, указывающее член для настройки.
Пример:
var mock = new Mock<ICalculator>();
mock.Setup(calc => calc.Add(It.IsAny<int>(), It.IsAny<int>()))
.Returns((int a, int b) => a + b);
2. Mock<T>.SetupGet() / SetupSet()
Что делает: Настраивает поведение getter'ов и setter'ов свойств.
Параметры: Лямбда для доступа к свойству.
Пример:
var mock = new Mock<ISettings>();
mock.SetupGet(s => s.Timeout)
.Returns(5000);
mock.SetupSet(s => s.Timeout = It.IsAny<int>())
.Callback<int>(value => Console.WriteLine($"Timeout set to {value}"));
3. Mock<T>.Returns()
Что делает: Задает возвращаемое значение для метода. Может возвращать фиксированное значение, результат функции или генерировать последовательность значений.
Параметры: Значение или функция, возвращающая значение.
Пример:
// Фиксированное значение
mock.Setup(x => x.GetCount()).Returns(42);
// Функция с аргументами
mock.Setup(x => x.Calculate(It.IsAny<int>()))
.Returns((int n) => n * 2);
// Последовательность значений
mock.SetupSequence(x => x.GetNext())
.Returns(1)
.Returns(2)
.Throws(new InvalidOperationException());
4. Mock<T>.Throws()
Что делает: Заставляет метод выбрасывать исключение при вызове. Используется для тестирования обработки ошибок.
Параметры: Исключение или тип исключения.
Пример:
mock.Setup(x => x.Connect("bad_url"))
.Throws<HttpRequestException>();
// С конкретным исключением
mock.Setup(x => x.Connect("invalid"))
.Throws(new ArgumentException("Invalid URL"));
5. Mock<T>.Verify()
Что делает: Проверяет, что метод был вызван (или не был вызван) с определенными аргументами и определенное количество раз. Это сердце поведенческого тестирования.
Параметры: Лямбда с вызовом метода, опционально Times и строка сообщения об ошибке.
Пример:
// Проверка, что метод вызван ровно 1 раз
mock.Verify(x => x.Save(It.IsAny<string>()), Times.Once);
// Проверка, что метод не вызывался
mock.Verify(x => x.Delete(It.IsAny<int>()), Times.Never);
// Проверка с конкретными аргументами
mock.Verify(x => x.Update("key", "value"), Times.AtLeastOnce);
// Проверка с кастомным сообщением об ошибке
mock.Verify(x => x.Process(), Times.Exactly(3), "Process should be called 3 times");
6. Mock<T>.Callback()
Что делает: Выполняет пользовательский код при вызове метода. Полезно для логирования, проверки аргументов или изменения состояния.
Параметры: Делегат или лямбда, которая будет выполнена.
Пример:
var capturedArgs = new List<string>();
mock.Setup(x => x.Log(It.IsAny<string>()))
.Callback<string>(msg => capturedArgs.Add(msg));
mock.Object.Log("First");
mock.Object.Log("Second");
Assert.Equal(2, capturedArgs.Count);
Assert.Equal("First", capturedArgs[0]);
7. It.IsAny<T>()
Что делает: Сопоставляется с любым значением указанного типа. Используется в Setup и Verify для игнорирования конкретных аргументов.
Параметры: Тип T.
Пример:
// Любой int
mock.Setup(x => x.Add(It.IsAny<int>(), 5)).Returns(10);
// Любая строка, не null
mock.Setup(x => x.Find(It.IsNotNull<string>())).Returns(true);
// Любое число больше 100
mock.Setup(x => x.Process(It.Is<int>(n => n > 100))).Returns("Big");
8. It.IsInRange<T>()
Что делает: Сопоставляется со значениями в заданном диапазоне (включительно или исключительно).
Параметры: Нижняя граница, верхняя граница, Range.Inclusive или Range.Exclusive.
Пример:
mock.Setup(x => x.SetVolume(It.IsInRange(0, 100, Range.Inclusive)))
.Returns(true);
mock.Setup(x => x.SetVolume(It.IsInRange(-10, -1, Range.Exclusive)))
.Throws<ArgumentOutOfRangeException>();
9. Mock<T>.Object
Что делает: Возвращает экземпляр mock-объекта, который реализует интерфейс T. Именно этот объект передается в тестируемый код.
Параметры: Нет.
Пример:
var mock = new Mock<IDatabase>();
IDatabase db = mock.Object; // Это "фальшивая" база данных
var service = new DataService(db);
10. MockBehavior
Что делает: Определяет, как mock ведет себя при вызове ненастроенных методов. Есть три режима: Loose (возвращает default значения), Strict (бросает исключение), Default (Loose).
Параметры: MockBehavior enum.
Пример:
// Strict: любой незапланированный вызов упадет с исключением
var strictMock = new Mock<IService>(MockBehavior.Strict);
strictMock.Setup(x => x.DoSomething()).Returns(true);
// strictMock.Object.DoSomethingElse(); // Exception!
// Loose: ненастроенные методы возвращают default
var looseMock = new Mock<IService>(MockBehavior.Loose);
int result = looseMock.Object.GetNumber(); // 0 (default for int)
11. Mock<T>.SetupAllProperties()
Что делает: Автоматически настраивает все свойства так, чтобы они работали как "заглушки" — можно устанавливать и получать значения без дополнительного Setup.
Параметры: Нет.
Пример:
public interface IConfig
{
string Name { get; set; }
int Port { get; set; }
}
var mock = new Mock<IConfig>();
mock.SetupAllProperties();
mock.Object.Name = "Test";
mock.Object.Port = 8080;
Assert.Equal("Test", mock.Object.Name);
Assert.Equal(8080, mock.Object.Port);
Реальный практический пример: тестирование сервиса отправки заказов
Рассмотрим более сложный сценарий: сервис обработки заказов, который зависит от репозитория, email-сервиса и логгера.
public interface IOrderRepository
{
Order GetById(int id);
void Update(Order order);
}
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
public interface ILogger
{
void Log(string message);
}
public class Order
{
public int Id { get; set; }
public string CustomerEmail { get; set; }
public decimal Total { get; set; }
public bool IsProcessed { get; set; }
}
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly IEmailService _email;
private readonly ILogger _logger;
public OrderService(I