WireMock.Net для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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. Советы по производительности и типичные ошибки

Советы по производительности:

Рекомендаций