본문으로 건너뛰기
개발 뉴스로
Backenddev.to··원문 약 3

올바른 방법으로 HttpClient 테스트: 인터페이스가 아닌 모의 핸들러

Testing HttpClient the Right Way: Mock Handlers, Not Interfaces

테스트를 👋 사랑하는 개발자 여러분!

핵심 요약

자동 요약
  1. 1테스트를 👋 사랑하는 개발자 여러분!
  2. 2이 패턴이 항상 보입니다://잘못된 ❌ 방식의 공용 인터페이스 IHttpClientWrapper {작업 GetAsync (문자열 URL); 작업 PostAsync…
  3. 3HTTP 호출을 테스트하는 훨씬 더 좋은 방법이 있습니다.

원문 본문

출처 · dev.to

Hey test-loving developers! 👋

I see this pattern ALL the time:

// ❌ The wrong way public interface IHttpClientWrapper { Task<T> GetAsync<T>(string url); Task<T> PostAsync<T>(string url, object data); } 

Stop wrapping HttpClient! There's a much better way to test HTTP calls. Let me show you!

The Problem with Wrapper Interfaces

  1. You're testing your wrapper, not real behavior — Mocking IHttpClientWrapper.GetAsync<User>() doesn't test serialization, headers, or error handling.

  2. You lose HttpClient features — Timeouts, handlers, resilience policies... all gone behind your abstraction.

  3. It's unnecessaryHttpClient is already designed for testability!

The Right Way: Mock the Handler

HttpClient takes an HttpMessageHandler in its constructor. That's your test seam!

public class MockHttpMessageHandler : HttpMessageHandler { private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handler; public MockHttpMessageHandler( Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) { _handler = handler; } protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return _handler(request, cancellationToken); } } 

🎯 Fun Fact: The handler pattern is why HttpClient implements IDisposable but you're told not to dispose it frequently. The handler does the real work and is what's expensive to create/destroy!

Your First Mock Test

[Fact] public async Task GetUser_ReturnsUser_WhenApiSucceeds() { // Arrange var expectedUser = new User { Id = 1, Name = "John" }; var handler = new MockHttpMessageHandler((request, ct) => { Assert.Equal(HttpMethod.Get, request.Method); Assert.Equal("/api/users/1", request.RequestUri?.PathAndQuery); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(expectedUser) }; return Task.FromResult(response); }); var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.example.com") }; var userClient = new UserApiClient(client); // Act var user = await userClient.GetByIdAsync(1); // Assert Assert.NotNull(user); Assert.Equal(expectedUser.Id, user.Id); Assert.Equal(expectedUser.Name, user.Name); } 

Using Moq (More Flexible)

[Fact] public async Task GetUser_Throws_WhenApiReturns500() { // Arrange var handlerMock = new Mock<HttpMessageHandler>(); handlerMock .Protected() .Setup<Task<HttpResponseMessage>>( "SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()) .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError) { Content = new StringContent("Server error") }); var client = new HttpClient(handlerMock.Object) { BaseAddress = new Uri("https://api.example.com") }; var userClient = new UserApiClient(client); // Act & Assert await Assert.ThrowsAsync<HttpRequestException>( () => userClient.GetByIdAsync(1)); } 

A Reusable Test Helper

public static class HttpClientTestHelper { public static HttpClient CreateMockClient<TResponse>( TResponse response, HttpStatusCode statusCode = HttpStatusCode.OK, Action<HttpRequestMessage>? requestValidator = null) { var handler = new MockHttpMessageHandler((request, ct) => { requestValidator?.Invoke(request); return Task.FromResult(new HttpResponseMessage(statusCode) { Content = JsonContent.Create(response) }); }); return new HttpClient(handler) { BaseAddress = new Uri("https://test.example.com") }; } public static HttpClient CreateErrorClient( HttpStatusCode statusCode, string? errorMessage = null) { var handler = new MockHttpMessageHandler((request, ct) => { return Task.FromResult(new HttpResponseMessage(statusCode) { Content = errorMessage != null ? new StringContent(errorMessage) : null }); }); return new HttpClient(handler) { BaseAddress = new Uri("https://test.example.com") }; } } 

Clean Tests

[Fact] public async Task GetUser_ReturnsUser() { // Arrange var expected = new User { Id = 1, Name = "John" }; var client = HttpClientTestHelper.CreateMockClient(expected); var userClient = new UserApiClient(client); // Act var user = await userClient.GetByIdAsync(1); // Assert Assert.Equal(expected.Id, user?.Id); } [Fact] public async Task GetUser_ReturnsNull_WhenNotFound() { // Arrange var client = HttpClientTestHelper.CreateErrorClient(HttpStatusCode.NotFound); var userClient = new UserApiClient(client); // Act var user = await userClient.GetByIdAsync(999); // Assert Assert.Null(user); } 

💡 Testing with WebApplicationFactory (Integration)

For integration tests, use the real HTTP stack:

public class UserApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client; public UserApiIntegrationTests(WebApplicationFactory<Program> factory) { _client = factory.CreateClient(); } [Fact] public async Task GetUsers_ReturnsUsers() { // Act var users = await _client.GetFromJsonAsync<List<User>>("/api/users"); // Assert Assert.NotNull(users); Assert.NotEmpty(users); } } 

Testing Resilience Policies

Want to verify your retry logic works?

[Fact] public async Task Client_RetriesOnTransientFailure() { // Arrange var callCount = 0; var handler = new MockHttpMessageHandler((request, ct) => { callCount++; // Fail first 2 times, succeed on 3rd if (callCount < 3) { return Task.FromResult( new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); } return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = JsonContent.Create(new User { Id = 1 }) }); }); var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.example.com") }; // Add resilience (in real code, use IHttpClientFactory) var userClient = new UserApiClient(client); // Act var user = await userClient.GetByIdAsync(1); // Assert Assert.Equal(3, callCount); // Verified it retried Assert.NotNull(user); } 

Testing Request Content

[Fact] public async Task CreateUser_SendsCorrectPayload() { // Arrange HttpRequestMessage? capturedRequest = null; var handler = new MockHttpMessageHandler(async (request, ct) => { capturedRequest = request; return new HttpResponseMessage(HttpStatusCode.Created) { Content = JsonContent.Create(new User { Id = 1, Name = "John" }) }; }); var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.example.com") }; var userClient = new UserApiClient(client); // Act var createRequest = new CreateUserRequest { Name = "John", Email = "john@test.com" }; var user = await userClient.CreateAsync(createRequest); // Assert Assert.NotNull(capturedRequest); Assert.Equal(HttpMethod.Post, capturedRequest.Method); var body = await capturedRequest.Content!.ReadFromJsonAsync<CreateUserRequest>(); Assert.Equal("John", body?.Name); Assert.Equal("john@test.com", body?.Email); } 

Cheat Sheet: What to Test

Test Approach Happy path Mock handler returns expected data Error handling Mock handler returns error codes Request format Capture and inspect request Retry logic Count handler invocations Timeout behavior Mock handler with Task.Delay Headers Inspect request.Headers in mock Integration WebApplicationFactory

Wrapping Up

Stop creating wrapper interfaces around HttpClient. The built-in handler pattern is:

  • More realistic (tests actual serialization)
  • More flexible (test any HTTP behavior)
  • More maintainable (no fake abstraction layer)

Mock the handler, test the real client! 🎯

Happy testing! 🚀

For further actions, you may consider blocking this person and/or reporting abuse

이 글은 dev.to 의 원문을 정제해 보여드립니다. 저작권은 원저작자에게 있습니다.

#backend#csharp#dotnet#testing

전체 내용이 궁금하다면

dev.to 원문에서 이어 읽기

원문 보기

비슷한 글

5유사도 추천