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
Saturday, 29 November 2025
Під час перевірки коду, який використовує HttpClient, традиційний приступ включає насмішки HttpMessageHandler В той час, як це працює, воно може бути докладним, обтяжливим, і, відверто кажучи, трохи потворним. DelegatingHandler для створення тестових обробників, які поводяться як справжні кінцеві точки HTTP.
В цьому полі я покажу вам чому ви можете пропустити глузування повністю і використовувати DelegatingHandler для більш придатного для читання, збереження і компактного тестового коду.
Ось яке типове значення 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);
У цьому є декілька проблем:
Protected() і ItExpr тому що SendAsync захищено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 все ще може бути доречним:
Verify() є корисним для виконання дзвінківДля перевірки ви можете додати його до 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 надасть вам:
Наступного разу, коли ви досягнете Mock<HttpMessageHandler>, Обдумайте чи це не є просто DelegatingHandler Ваше майбутнє " я " (і ваші колеги з команди) подякуватимуть вам за чистіший та придатніший тестовий код.
Див. проекти-тести в цьому розв'язку для прикладів реального світу цього зразка у дії.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.