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

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

HttpClient Unit Testing xUnit

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

Saturday, 29 November 2025

Вступ

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

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

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

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

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:

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")
        };
    }
}

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

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

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

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

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

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 (які ви маєте бути), програма обробки тестів є простою:

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;
}

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

[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"));
}

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

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

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);
    }
}

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

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 також:

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 Ваше майбутнє " я " (і ваші колеги з команди) подякуватимуть вам за чистіший та придатніший тестовий код.

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

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.