WireMock.Net для C# (.NET): Полное руководство
1. Что это и зачем нужна?
WireMock.Net — это мощная библиотека для создания HTTP mock-серверов (заглушек) в экосистеме .NET. Она является прямой C#-реализацией популярного Java-проекта WireMock. Основная задача библиотеки — эмулировать поведение реальных HTTP/HTTPS сервисов (REST API, SOAP, gRPC и т.д.) во время тестирования или разработки.
Зачем это нужно? В современной микросервисной архитектуре ваше приложение часто зависит от десятков внешних API. Использование реальных сервисов в юнит- или интеграционных тестах приводит к ряду проблем: нестабильность тестов из-за недоступности сервера, медленная работа, финансовые затраты на вызовы платных API и невозможность тестировать редкие сценарии (например, ошибку 500 или таймаут). WireMock.Net решает эти проблемы, позволяя вам «подменить» внешний сервис локально.
Ключевые сценарии использования: модульное тестирование клиентов HTTP (HttpClient), интеграционное тестирование веб-приложений, разработка «офлайн» (когда бэкенд еще не готов), тестирование обработки ошибок и таймаутов, а также создание изолированных тестовых сред (sandbox).
2. Установка
Библиотека распространяется через NuGet. Для установки используйте одну из следующих команд:
Через .NET CLI (рекомендуется):
dotnet add package WireMock.Net
Через Package Manager Console (Visual Studio):
Install-Package WireMock.Net
Для работы с асинхронными тестами и Fluent Assertions (опционально, но полезно):
dotnet add package WireMock.Net.FluentAssertions
Примечание: В отличие от Python (pip) или Node.js (npm), в .NET используется менеджер пакетов NuGet.
3. Быстрый старт — минимальный рабочий пример
Создайте консольное приложение или тестовый проект (xUnit/NUnit). Ниже приведен минимальный код, который запускает mock-сервер на порту 9876, возвращающий JSON-ответ на GET-запрос.
using WireMock.Server;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
// 1. Запускаем сервер (по умолчанию на случайном порту, но мы укажем конкретный)
var server = WireMockServer.Start(9876);
// 2. Настраиваем заглушку (stub)
server.Given(
Request.Create()
.WithPath("/api/users/1")
.UsingGet()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBody("{ \"id\": 1, \"name\": \"John Doe\" }")
);
Console.WriteLine($"Mock server running on {server.Urls[0]}");
Console.WriteLine("Press any key to stop...");
Console.ReadKey();
// 3. Останавливаем сервер
server.Stop();
После запуска откройте браузер или используйте curl: curl http://localhost:9876/api/users/1. Вы получите JSON-ответ.
4. Основные методы/функции/классы
4.1. WireMockServer.Start()
Сигнатура: public static WireMockServer Start(int? port = null, bool ssl = false)
Описание: Статический метод, который создает и запускает экземпляр mock-сервера. Если порт не указан, выбирается случайный свободный порт.
// Запуск на порту 5000
var server = WireMockServer.Start(5000);
// Запуск на случайном порту
var serverRandom = WireMockServer.Start();
4.2. server.Given()
Сигнатура: public IRespondWithAwareResponseBuilder Given(IRequestBuilder requestBuilder)
Описание: Основной метод для настройки правил (stubs). Принимает построитель запроса (RequestBuilder) и возвращает построитель ответа (ResponseBuilder).
server.Given(
Request.Create().WithPath("/weather").UsingGet()
).RespondWith(
Response.Create().WithBody("Sunny")
);
4.3. Request.Create()
Сигнатура: public static IRequestBuilder Create()
Описание: Статический метод, создающий новый экземпляр построителя запроса. Позволяет задать метод, путь, заголовки, параметры запроса и тело.
var request = Request.Create()
.WithPath("/api/items")
.UsingPost()
.WithBody("{\"name\":\"test\"}");
4.4. .WithPath()
Сигнатура: public IRequestBuilder WithPath(string path)
Описание: Задает путь URL, на который должен прийти запрос. Поддерживает регулярные выражения и wildcard-шаблоны.
// Точное совпадение
Request.Create().WithPath("/exact/path");
// Совпадение по шаблону (regex)
Request.Create().WithPath("/*/users/*");
4.5. .WithParam()
Сигнатура: public IRequestBuilder WithParam(string key, params string[] values)
Описание: Добавляет условие на наличие query-параметра в запросе. Можно указать одно или несколько ожидаемых значений.
Request.Create()
.WithPath("/search")
.WithParam("q", "dotnet")
.WithParam("page", "1");
4.6. Response.Create()
Сигнатура: public static IResponseBuilder Create()
Описание: Статический метод, создающий новый экземпляр построителя ответа. Позволяет задать статус-код, заголовки, тело ответа и задержки.
var response = Response.Create()
.WithStatusCode(201)
.WithHeader("Location", "/api/items/123");
4.7. .WithBodyAsJson()
Сигнатура: public IResponseBuilder WithBodyAsJson(object body)
Описание: Удобный метод для возврата JSON-ответа. Автоматически сериализует переданный объект в JSON и устанавливает заголовок Content-Type: application/json.
var user = new { Id = 1, Name = "Alice" };
server.Given(Request.Create().WithPath("/user").UsingGet())
.RespondWith(Response.Create().WithBodyAsJson(user));
4.8. .WithDelay()
Сигнатура: public IResponseBuilder WithDelay(TimeSpan delay)
Описание: Добавляет искусственную задержку перед отправкой ответа. Полезно для тестирования таймаутов и поведения при медленных сетях.
// Задержка в 5 секунд
server.Given(Request.Create().WithPath("/slow"))
.RespondWith(Response.Create()
.WithBody("Delayed response")
.WithDelay(TimeSpan.FromSeconds(5)));
4.9. server.ResetMappings()
Сигнатура: public void ResetMappings()
Описание: Очищает все ранее настроенные заглушки (stubs). Полезно для сброса состояния между тестами.
server.ResetMappings(); // Удаляет все правила
4.10. server.LogEntries
Сигнатура: public IList<LogEntry> LogEntries { get; }
Описание: Свойство, возвращающее список всех запросов, которые были отправлены на сервер. Используется для верификации (assertions) в тестах.
// Проверяем, что запрос был сделан
var logs = server.LogEntries;
Assert.Single(logs);
Assert.Equal("/api/test", logs[0].RequestMessage.Path);
5. Реальный практический пример (150+ строк кода)
Рассмотрим сценарий: у нас есть микросервис OrderService, который при создании заказа вызывает внешний API PaymentGateway для списания средств. Мы напишем интеграционный тест, который изолирует OrderService от реального PaymentGateway с помощью WireMock.Net.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using WireMock.Server;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using Xunit;
using FluentAssertions;
public class PaymentGatewayMockTests : IDisposable
{
private readonly WireMockServer _mockServer;
private readonly HttpClient _httpClient;
public PaymentGatewayMockTests()
{
// 1. Запускаем mock-сервер на случайном порту
_mockServer = WireMockServer.Start();
_httpClient = new HttpClient();
}
[Fact]
public async Task CreateOrder_WhenPaymentSuccessful_ReturnsOrderId()
{
// Arrange: Настраиваем заглушку для успешного платежа
_mockServer.Given(
Request.Create()
.WithPath("/api/v1/charge")
.UsingPost()
.WithBody("{\"amount\": 100, \"currency\": \"USD\"}")
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithBodyAsJson(new { transactionId = "txn_123", status = "success" })
);
// Создаем реальный сервис, который использует HttpClient, указывающий на наш mock
var paymentService = new PaymentService(_httpClient, _mockServer.Urls[0]);
// Act
var result = await paymentService.ChargeAsync(100, "USD");
// Assert
result.Should().NotBeNull();
result.TransactionId.Should().Be("txn_123");
result.Status.Should().Be("success");
// Верификация: проверяем, что запрос был сделан ровно 1 раз
_mockServer.LogEntries.Should().HaveCount(1);
}
[Fact]
public async Task CreateOrder_WhenPaymentFails_ReturnsError()
{
// Arrange: Настраиваем заглушку для ошибки 500
_mockServer.Given(
Request.Create()
.WithPath("/api/v1/charge")
.UsingPost()
)
.RespondWith(
Response.Create()
.WithStatusCode(500)
.WithBody("{\"error\": \"insufficient_funds\"}")
);
var paymentService = new PaymentService(_httpClient, _mockServer.Urls[0]);
// Act & Assert
var exception = await Assert.ThrowsAsync(() =>
paymentService.ChargeAsync(100, "USD"));
exception.Message.Should().Contain("insufficient_funds");
}
[Fact]
public async Task CreateOrder_WhenPaymentTimeout_ReturnsTimeoutError()
{
// Arrange: Настраиваем заглушку с задержкой в 10 секунд (имитация таймаута)
_mockServer.Given(
Request.Create()
.WithPath("/api/v1/charge")
.UsingPost()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithDelay(TimeSpan.FromSeconds(10))
);
var httpClientWithTimeout = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
var paymentService = new PaymentService(httpClientWithTimeout, _mockServer.Urls[0]);
// Act & Assert
await Assert.ThrowsAsync(() =>
paymentService.ChargeAsync(100, "USD"));
}
public void Dispose()
{
_mockServer?.Stop();
_httpClient?.Dispose();
}
}
// Вспомогательные классы для примера
public class PaymentService
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
public PaymentService(HttpClient httpClient, string baseUrl)
{
_httpClient = httpClient;
_baseUrl = baseUrl;
}
public async Task ChargeAsync(decimal amount, string currency)
{
var requestBody = $"{{\"amount\": {amount}, \"currency\": \"{currency}\"}}";
var content = new StringContent(requestBody, System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{_baseUrl}/api/v1/charge", content);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
throw new PaymentException(errorBody);
}
var json = await response.Content.ReadAsStringAsync();
return System.Text.Json.JsonSerializer.Deserialize(json);
}
}
public class PaymentResult
{
public string TransactionId { get; set; }
public string Status { get; set; }
}
public class PaymentException : Exception
{
public PaymentException(string message) : base(message) { }
}
6. Советы по производительности и типичные ошибки
Советы по производительности:
- Переиспользуйте сервер: В рамках одного тестового класса создавайте один экземпляр WireMockServer (через IClassFixture в xUnit) и сбрасывайте только маппинги через
ResetMappings(). Запуск и остановка сервера для каждого теста — дорогая операция. - Используйте асинхронные методы: WireMock.Net полностью асинхронен. Используйте async/await в тестах, чтобы не блокировать потоки.
-
Также в библиотеке
Prometheus метрики в TypeScript: полное руководство по client.js
Авторизация и аутентификация в Node.js: JWT, Passport.js, bcrypt
gRPC для JavaScript (Node.js): библиотеки, настройка и примеры кода
Реактивное программирование в Java: библиотеки Project Reactor и RxJava
GraphQL библиотека TypeScript: полное руководство по инструментам