단위 테스트에서 HttpClient 모의 }

단위 테스트에 사용할 코드를 래핑하는 데 몇 가지 문제가 있습니다. 문제는 이것입니다. IHttpHandler 인터페이스가 있습니다.

public interface IHttpHandler
{
    HttpClient client { get; }
}

그리고 그것을 사용하는 클래스, HttpHandler :

public class HttpHandler : IHttpHandler
{
    public HttpClient client
    {
        get
        {
            return new HttpClient();
        }
    }
}

그런 다음 클라이언트 구현을 삽입하기 위해 simpleIOC를 사용하는 Connection 클래스 :

public class Connection
{
    private IHttpHandler _httpClient;

    public Connection(IHttpHandler httpClient)
    {
        _httpClient = httpClient;
    }
}

그리고이 클래스가있는 단위 테스트 프로젝트가 있습니다.

private IHttpHandler _httpClient;

[TestMethod]
public void TestMockConnection()
{
    var client = new Connection(_httpClient);

    client.doSomething();  

    // Here I want to somehow create a mock instance of the http client
    // Instead of the real one. How Should I approach this?     

}

이제 분명히 백엔드에서 데이터 (JSON)를 검색하는 Connection 클래스에 메서드가 있습니다. 그러나 저는이 클래스에 대한 단위 테스트를 작성하고 싶습니다. 분명히 실제 백엔드에 대한 테스트를 작성하고 싶지 않습니다. 나는 큰 성공없이 이것에 대한 좋은 대답을 구글에 시도했다. 나는 Moq를 사용하여 이전에 조롱했지만 httpClient와 같은 것은 결코 사용하지 않았습니다. 이 문제에 어떻게 접근해야합니까?

미리 감사드립니다.



답변

인터페이스는 구체적인 HttpClient클래스를 노출하므로이 인터페이스 를 사용하는 모든 클래스가 여기에 연결됩니다. 즉, 모의 할 수 없습니다.

HttpClient인터페이스에서 상속되지 않으므로 직접 작성해야합니다. 데코레이터와 같은 패턴을 제안합니다 .

public interface IHttpHandler
{
    HttpResponseMessage Get(string url);
    HttpResponseMessage Post(string url, HttpContent content);
    Task<HttpResponseMessage> GetAsync(string url);
    Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}

수업은 다음과 같습니다.

public class HttpClientHandler : IHttpHandler
{
    private HttpClient _client = new HttpClient();

    public HttpResponseMessage Get(string url)
    {
        return GetAsync(url).Result;
    }

    public HttpResponseMessage Post(string url, HttpContent content)
    {
        return PostAsync(url, content).Result;
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }

    public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
    {
        return await _client.PostAsync(url, content);
    }
}

이 모든 것의 요점은 HttpClientHandler자체 를 생성하는 것 HttpClient입니다. 물론 IHttpHandler다른 방식으로 구현하는 여러 클래스를 생성 할 수 있습니다 .

이 접근 방식의 주요 문제는 다른 클래스의 메서드를 호출하는 클래스를 효과적으로 작성하고 있지만 상속 하는 클래스를 만들 수 있다는 것 입니다 HttpClient( Nkosi의 예제 참조 , 저보다 훨씬 나은 접근 방식입니다). HttpClient조롱 할 수있는 인터페이스가 있다면 인생은 훨씬 더 쉬울 것 입니다. 불행히도 그렇지 않습니다.

그러나이 예는 골든 티켓 이 아닙니다 . IHttpHandler여전히 네임 스페이스에 HttpResponseMessage속하는 에 의존 System.Net.Http하므로, 이외의 다른 구현이 필요한 경우 HttpClient응답을 HttpResponseMessage객체 로 변환하기 위해 일종의 매핑을 수행해야 합니다. 이것은 물론 여러 구현을 사용해야하는 경우 에만 문제 가 IHttpHandler되지만 그렇게하는 것처럼 보이지는 않으므로 세상의 끝은 아니지만 생각해 볼 사항입니다.

어쨌든, 추상화 된 IHttpHandler구체적인 HttpClient클래스 에 대해 걱정할 필요없이 간단히 조롱 할 수 있습니다.

비동기 메서드를 여전히 호출하지만 비동기 메서드 단위 테스트에 대해 걱정할 필요가 없으므로 비동기 메서드를 테스트하는 것이 좋습니다. 여기를 참조 하세요.


답변

HttpClient의 확장 성은 HttpMessageHandler생성자에 전달 된 것입니다. 그 의도는 플랫폼 별 구현을 허용하는 것이지만이를 모의 할 수도 있습니다. HttpClient에 대한 데코레이터 래퍼를 만들 필요가 없습니다.

Moq를 사용하는 것보다 DSL을 선호하는 경우 GitHub / Nuget에 라이브러리가있어 작업이 좀 더 쉬워집니다. https://github.com/richardszalay/mockhttp

var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}


답변

가장 좋은 방법은 HttpClient를 래핑하는 대신 HttpMessageHandler를 모의하는 것입니다. 이 대답은 여전히 ​​HttpClient를 주입하여 단일 항목이되거나 종속성 주입으로 관리 될 수 있다는 점에서 고유합니다.

“HttpClient는 한 번 인스턴스화되고 응용 프로그램 수명 내내 재사용되도록 고안되었습니다.” ( 출처 ).

SendAsync가 보호되기 때문에 HttpMessageHandler를 조롱하는 것은 약간 까다로울 수 있습니다. 다음은 xunit과 Moq를 사용한 완전한 예입니다.

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq

namespace MockHttpClient {
    class Program {
        static void Main(string[] args) {
            var analyzer = new SiteAnalyzer(Client);
            var size = analyzer.GetContentSize("http://microsoft.com").Result;
            Console.WriteLine($"Size: {size}");
        }

        private static readonly HttpClient Client = new HttpClient(); // Singleton
    }

    public class SiteAnalyzer {
        public SiteAnalyzer(HttpClient httpClient) {
            _httpClient = httpClient;
        }

        public async Task<int> GetContentSize(string uri)
        {
            var response = await _httpClient.GetAsync( uri );
            var content = await response.Content.ReadAsStringAsync();
            return content.Length;
        }

        private readonly HttpClient _httpClient;
    }

    public class SiteAnalyzerTests {
        [Fact]
        public async void GetContentSizeReturnsCorrectLength() {
            // Arrange
            const string testContent = "test content";
            var mockMessageHandler = new Mock<HttpMessageHandler>();
            mockMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(testContent)
                });
            var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));

            // Act
            var result = await underTest.GetContentSize("http://anyurl");

            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}


답변

이것은 일반적인 질문이며 HttpClient를 조롱하는 기능을 원하고 있었지만 마침내 HttpClient를 조롱해서는 안된다는 것을 깨닫게 된 것 같습니다. 그렇게하는 것이 논리적으로 보이지만, 오픈 소스 라이브러리에서 볼 수있는 것들에 세뇌 당했다고 생각합니다.

우리는 종종 “클라이언트”를 보게되는데, 코드에서 모방하여 격리 된 상태에서 테스트 할 수 있으므로 동일한 원칙을 HttpClient에 자동으로 적용하려고합니다. HttpClient는 실제로 많은 일을합니다. 당신은 그것을 HttpMessageHandler의 관리자로 생각할 수 있으므로 그것을 조롱하고 싶지 않으며, 그것이 여전히 인터페이스가없는 이유 입니다. 단위 테스트 또는 서비스 설계에 대해 정말로 관심이있는 부분은 응답을 반환하는 HttpMessageHandler이며이를 조롱 할 수 있습니다 .

또한 HttpClient를 더 큰 거래로 취급하기 시작해야한다는 점도 지적 할 가치가 있습니다. 예 : 새 HttpClients를 최소한으로 유지하십시오. 재사용하면 재사용이 가능하도록 설계되었으며 그렇게 할 경우 자원을 덜 사용합니다. 당신이 그것을 더 큰 거래처럼 다루기 시작하면, 그것을 조롱하고 싶어하는 것이 훨씬 더 잘못 느껴질 것이고 이제 메시지 핸들러는 클라이언트가 아닌 당신이 주입하는 것이 될 것입니다.

즉, 클라이언트 대신 핸들러에 대한 종속성을 설계하십시오. 더 좋은 점은 HttpClient를 사용하는 추상 “서비스”로, 핸들러를 주입하고 대신 주입 가능한 종속성으로 사용할 수 있습니다. 그런 다음 테스트에서 핸들러를 가짜로 만들어 테스트 설정에 대한 응답을 제어 할 수 있습니다.

HttpClient를 래핑하는 것은 엄청난 시간 낭비입니다.

업데이트 : Joshua Dooms의 예를 참조하십시오. 바로 제가 추천하는 것입니다.


답변

또한 주석에서 언급했듯이을 추상화 하여 HttpClient결합되지 않도록해야합니다. 나는 과거에 비슷한 일을했습니다. 나는 당신이하려는 일로 내가 한 일을 조정하려고 노력할 것입니다.

먼저 HttpClient클래스를 살펴보고 필요한 기능을 결정했습니다.

여기에 가능성이 있습니다.

public interface IHttpClient {
    System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}

앞서 언급했듯이 이것은 특정 목적을위한 것입니다. 나는 모든 것을 다루는 것에 대한 대부분의 의존성을 완전히 추상화 HttpClient하고 내가 원하는 것에 집중했습니다. 원하는 HttpClient기능 만 제공 하려면 을 추상화하는 방법을 평가해야합니다 .

이제 테스트에 필요한 것만 조롱 할 수 있습니다.

나는 IHttpHandler완전히 제거 하고 HttpClient추상화를 사용하는 것이 좋습니다 IHttpClient. 그러나 처리기 인터페이스의 본문을 추상화 된 클라이언트의 구성원으로 바꿀 수 있으므로 선택하지 않습니다.

IHttpClient그런 다음 의 구현을 사용하여 실제 / 콘크리트 HttpClient또는 해당 문제에 대한 다른 개체 를 래핑 / 적용 할 수 있습니다. 이는 HTTP 요청을 만드는 데 사용할 수 있습니다 HttpClient. 이는 구체적 으로 해당 기능을 제공하는 서비스였습니다 . 추상화를 사용하는 것은 깔끔하고 (내 의견) SOLID 접근 방식이며 프레임 워크가 변경됨에 따라 다른 것을 위해 기본 클라이언트를 전환해야하는 경우 코드를보다 유지 관리 할 수 ​​있습니다.

다음은 구현 방법에 대한 스 니펫입니다.

/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/> 
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
    HttpClient httpClient;

    public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
        httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
    }

    //...other code

     /// <summary>
    ///  Send a GET request to the specified Uri as an asynchronous operation.
    /// </summary>
    /// <typeparam name="T">Response type</typeparam>
    /// <param name="uri">The Uri the request is sent to</param>
    /// <returns></returns>
    public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
        var result = default(T);
        //Try to get content as T
        try {
            //send request and get the response
            var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
            //if there is content in response to deserialize
            if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
                //get the content
                string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                //desrialize it
                result = deserializeJsonToObject<T>(responseBodyAsText);
            }
        } catch (Exception ex) {
            Log.Error(ex);
        }
        return result;
    }

    //...other code
}

위의 예에서 볼 수 있듯이 일반적으로 사용과 관련된 많은 무거운 작업 HttpClient이 추상화 뒤에 숨겨져 있습니다.

그런 다음 연결 클래스를 추상화 된 클라이언트로 주입 할 수 있습니다.

public class Connection
{
    private IHttpClient _httpClient;

    public Connection(IHttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

그러면 테스트에서 SUT에 필요한 것을 모의 할 수 있습니다.

private IHttpClient _httpClient;

[TestMethod]
public void TestMockConnection()
{
    SomeModelObject model = new SomeModelObject();
    var httpClientMock = new Mock<IHttpClient>();
    httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
        .Returns(() => Task.FromResult(model));

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

    // Assuming doSomething uses the client to make
    // a request for a model of type SomeModelObject
    client.doSomething();
}


답변

다른 답변을 기반으로 외부 종속성이없는이 코드를 제안합니다.

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public async Task MyTestMethod()
    {
        var httpClient = new HttpClient(new MockHttpMessageHandler());

        var content = await httpClient.GetStringAsync("http://some.fake.url");

        Assert.AreEqual("Content as string", content);
    }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Content as string")
        };

        return await Task.FromResult(responseMessage);
    }
}


답변

문제는 당신이 그것을 조금 거꾸로 가지고 있다는 것입니다.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

    public AuroraClient() : this(new HttpClientHandler())
    {
    }

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}

위의 수업을 보면 이것이 원하는 것 같아요. Microsoft는 최적의 성능을 위해 클라이언트를 활성 상태로 유지할 것을 권장하므로 이러한 유형의 구조를 사용하면 그렇게 할 수 있습니다. 또한 HttpMessageHandler는 추상 클래스이므로 mockable입니다. 테스트 방법은 다음과 같습니다.

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mockMessageHandler = new Mock<HttpMessageHandler>();
    // Set up your mock behavior here
    var auroraClient = new AuroraClient(mockMessageHandler.Object);
    // Act
    // Assert
}

이를 통해 HttpClient의 동작을 조롱하면서 로직을 테스트 할 수 있습니다.

죄송합니다. 이것을 작성하고 직접 시도한 후 HttpMessageHandler에서 보호 된 메서드를 조롱 할 수 없다는 것을 깨달았습니다. 이어서 적절한 모의 삽입을 허용하기 위해 다음 코드를 추가했습니다.

public interface IMockHttpMessageHandler
{
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    private readonly IMockHttpMessageHandler _realMockHandler;

    public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
    {
        _realMockHandler = realMockHandler;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _realMockHandler.SendAsync(request, cancellationToken);
    }
}

이것으로 작성된 테스트는 다음과 같습니다.

[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
    // Arrange
    var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
    // Set up Mock behavior here.
    var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}