# Перевірка модулів HttpClient без мошок

<datetime class="hidden">2025-11-29T07:00</datetime>

<!--category-- xUnit, Unit Testing, HttpClient -->
## Вступ

Під час перевірки коду, який використовує `HttpClient`, традиційний приступ включає насмішки `HttpMessageHandler` В той час, як це працює, воно може бути докладним, обтяжливим, і, відверто кажучи, трохи потворним. `DelegatingHandler` для створення тестових обробників, які поводяться як справжні кінцеві точки HTTP.

В цьому полі я покажу вам чому ви можете пропустити глузування повністю і використовувати `DelegatingHandler` для більш придатного для читання, збереження і компактного тестового коду.

[TOC]

## Проблема з обдумуванням HttpMessageHandler

Ось яке типове значення `HttpMessageHandler` глузування виглядає, як у Мока:

```csharp
var mockHandler = new Mock<HttpMessageHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.ToString().Contains("api/send")),
        ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync((HttpRequestMessage request, CancellationToken cancellationToken) =>
    {
        var requestBody = request.Content?.ReadAsStringAsync(cancellationToken).Result;
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(requestBody ?? "No content", Encoding.UTF8, "application/json")
        };
    });

var client = new HttpClient(mockHandler.Object);
```

У цьому є декілька проблем:

1. **Докладний** - Багато бойлерів для того, що має бути простою поведінкою
2. **Церемонія захищеного методу** - Вам потрібно `Protected()` і `ItExpr` тому що `SendAsync` захищено
3. **Важко читати** - Настоящий тест логичность закопана в церемонии сбора.
4. **Не можна повторно з' єднувати** - Кожен тест потребує подібного коду
5. **БріттлCity in Germany** - Легко неправильно визначити назву методу, заснованого на рядку

## Альтернатива Делегування

`DelegatingHandler` є вбудованим класом .NET, розробленим саме з цією метою - перехоплення HTTP- запитів до того, як вони увійдуть до мережі. Це те саме, що використовується у процесі обробки даних, і обробки журналів для роботи з комп' ютерами.

Ось такі ж функціональні можливості, як і `DelegatingHandler`:

```csharp
public class EchoHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var content = request.Content != null
            ? await request.Content.ReadAsStringAsync(cancellationToken)
            : "No content";

        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(content, Encoding.UTF8, "application/json")
        };
    }
}
```

Використання:

```csharp
var client = new HttpClient(new EchoHandler());
```

І все. без назв методів, що базуються на струнах.

## Реальний приклад: опрацювання перекладацької служби

Ось більш витончений приклад програми перевірки перекладу:

```csharp
public class TranslateDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var absPath = request.RequestUri?.AbsolutePath;
        var method = request.Method;

        return absPath switch
        {
            "/translate" when method == HttpMethod.Post => await HandleTranslate(request),
            "/translate" => new HttpResponseMessage(HttpStatusCode.OK),
            "/health" => new HttpResponseMessage(HttpStatusCode.OK),
            _ => new HttpResponseMessage(HttpStatusCode.NotFound)
        };
    }

    private static async Task<HttpResponseMessage> HandleTranslate(HttpRequestMessage request)
    {
        var content = await request.Content!.ReadFromJsonAsync<TranslateRequest>();

        // Simulate error for specific test case
        if (content?.TargetLanguage == "xx")
            return new HttpResponseMessage(HttpStatusCode.InternalServerError);

        var response = new TranslateResponse("es", new[] { "Texto traducido" });
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(response)
        };
    }
}
```

Цей обробник:

- Маршрути різних шляхів поведінки
- Відхиляє запит на контент для прийняття рішень
- Повертає відповідні коди помилок для окремих сценаріїв
- Є повністю придатним для читання і самодокументування

## Налаштування залежностей

За використання `IHttpClientFactory` (які ви маєте бути), програма обробки тестів є простою:

```csharp
public static IServiceCollection SetupTestServices(DelegatingHandler handler)
{
    var services = new ServiceCollection();

    services.AddHttpClient<ITranslationService, TranslationService>(client =>
    {
        client.BaseAddress = new Uri("https://test.local");
    })
    .ConfigurePrimaryHttpMessageHandler(() => handler);

    return services;
}
```

Потім у тестах:

```csharp
[Fact]
public async Task Translate_ReturnsTranslatedText()
{
    var services = SetupTestServices(new TranslateDelegatingHandler());
    var provider = services.BuildServiceProvider();
    var service = provider.GetRequiredService<ITranslationService>();

    var result = await service.TranslateAsync("Hello", "es");

    Assert.Equal("Texto traducido", result);
}

[Fact]
public async Task Translate_InvalidLanguage_ThrowsException()
{
    var services = SetupTestServices(new TranslateDelegatingHandler());
    var provider = services.BuildServiceProvider();
    var service = provider.GetRequiredService<ITranslationService>();

    await Assert.ThrowsAsync<HttpRequestException>(
        () => service.TranslateAsync("Hello", "xx"));
}
```

## Розширений шаблон: придатні до налаштування обробники

Для гнучкості ви можете створити обробники, які приймуть налаштування:

```csharp
public class ConfigurableHandler : DelegatingHandler
{
    private readonly Dictionary<string, Func<HttpRequestMessage, Task<HttpResponseMessage>>> _routes;

    public ConfigurableHandler()
    {
        _routes = new Dictionary<string, Func<HttpRequestMessage, Task<HttpResponseMessage>>>();
    }

    public ConfigurableHandler WithRoute(string path, HttpStatusCode status)
    {
        _routes[path] = _ => Task.FromResult(new HttpResponseMessage(status));
        return this;
    }

    public ConfigurableHandler WithRoute(string path, object responseBody)
    {
        _routes[path] = _ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(responseBody)
        });
        return this;
    }

    public ConfigurableHandler WithRoute(
        string path,
        Func<HttpRequestMessage, Task<HttpResponseMessage>> handler)
    {
        _routes[path] = handler;
        return this;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var path = request.RequestUri?.AbsolutePath ?? "";

        if (_routes.TryGetValue(path, out var handler))
            return await handler(request);

        return new HttpResponseMessage(HttpStatusCode.NotFound);
    }
}
```

Використання:

```csharp
var handler = new ConfigurableHandler()
    .WithRoute("/api/users", new[] { new User("Alice"), new User("Bob") })
    .WithRoute("/api/health", HttpStatusCode.OK)
    .WithRoute("/api/error", HttpStatusCode.InternalServerError);

var client = new HttpClient(handler);
```

## Чому треба вживати делегації проти мошок?

Мок- заснований на монашці}Теаґтехнхендлер]
|--------|-------------------|-------------------|
| **Рядки коду** [.
| **Готовність до читання** Дз. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д.
| **Можливість повторного зв' язку** Дівчино
| **Зневадження** Дівчата:
| **Реорганізація** [Шуста]
| **Крива навчання** ♪Sieper (MOq APIs) } Minusal}
| **Залежності** Дзвінок Мокасонів None (вбудований-in)}

## Коли насмішки ще мають сенс

Якщо бути чесним, існують ситуації, коли висміювання у стилі Moq все ще може бути доречним:

1. **Одновідривні прості відповіді** - Якщо вам потрібен один репонент один раз, inline Moq може бути швидшим
2. **Перевірка** - Moq's `Verify()` є корисним для виконання дзвінків
3. **Існуюча база коду** - Якщо у вашої команди вже велика інфраструктура Мока.

Для перевірки ви можете додати його до DelagingHandler також:

```csharp
public class VerifyingHandler : DelegatingHandler
{
    public List<HttpRequestMessage> ReceivedRequests { get; } = new();

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        ReceivedRequests.Add(request);
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
    }
}
```

## Висновки

Користування `DelegatingHandler` для перевірки HttpClient надасть вам:

- **Ущільнений код** - Церемонія насмішок.
- **Тести, придатні для читання** - Звичайні класи C#
- **Змінні обробники** - Поділіться тестовими заняттями.
- **Легке усування вад** - Встановити точки зупину, пройти через код
- **Нуль залежностей** - Він збудований в НЕТ

Наступного разу, коли ви досягнете `Mock<HttpMessageHandler>`, Обдумайте чи це не є просто `DelegatingHandler` Ваше майбутнє " я " (і ваші колеги з команди) подякуватимуть вам за чистіший та придатніший тестовий код.

Див. проекти-тести в цьому розв'язку для прикладів реального світу цього зразка у дії.