Подтвердить что ты не робот

Издевательский HttpClient в модульных тестах

У меня есть некоторые проблемы, пытаясь обернуть мой код, который будет использоваться в модульных тестах. Проблемы в этом. У меня есть интерфейс IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

И класс, использующий его, HttpHandler:

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

И затем класс Connection, который использует simpleIOC для внедрения клиентской реализации:

public class Connection
{
    private IHttpHandler _httpClient;

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

И тогда у меня есть проект unit test, который имеет этот класс:

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?     

}

Теперь, очевидно, у меня будут методы в классе Connection, которые будут извлекать данные (JSON) из моего заднего конца. Тем не менее, я хочу написать модульные тесты для этого класса, и, очевидно, я не хочу писать тесты против реального конца, а скорее издеваться. Я попытался сделать Google хорошим ответом на этот вопрос без особого успеха. Я могу и использовал Moq, чтобы издеваться раньше, но никогда на что-то вроде httpClient. Как мне подойти к этой проблеме?

Спасибо заранее.

4b9b3361

Ответ 1

В вашем интерфейсе представлен конкретный класс 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, поскольку он был абстрагирован.

Я рекомендую протестировать методы non-async, поскольку они все еще называют асинхронные методы, но без хлопот беспокоиться об асинхронных методах тестирования модулей, см. здесь

Ответ 2

Расширяемость HttpClient заключается в HttpMessageHandler передаваемом конструктору. Его цель - разрешить реализацию для конкретной платформы, но вы также можете поиздеваться над ней. Там нет необходимости создавать оболочку декоратора для HttpClient.

Если вы предпочитаете DSL использованию Moq, у меня есть библиотека на 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'}

Ответ 3

Я согласен с некоторыми другими ответами, что лучший подход состоит в том, чтобы издеваться над HttpMessageHandler, а не оборачивать HttpClient. Этот ответ уникален тем, что он по-прежнему внедряет HttpClient, что позволяет ему быть одиночным или управляемым с внедрением зависимостей.

"HttpClient предназначен для создания экземпляра один раз и повторного использования в течение всего жизненного цикла приложения". (Источник).

Насмешка над HttpMessageHandler может быть немного хитрой, потому что SendAsync защищен. Вот полный пример с использованием 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);
        }
    }
}

Ответ 4

Это распространенный вопрос, и я сильно хотел побаловать себя умением HttpClient, но, думаю, я наконец понял, что вы не должны насмехаться над HttpClient. Это кажется логичным, но я думаю, что нам промыли мозги вещи, которые мы видим в библиотеках с открытым исходным кодом.

Мы часто видим "Клиентов", которые насмехаются в нашем коде, чтобы мы могли тестировать изолированно, поэтому мы автоматически пытаемся применить тот же принцип к HttpClient. HttpClient действительно много делает; вы можете думать об этом как о менеджере для HttpMessageHandler, так что вы не хотите насмехаться над этим, и поэтому у него до сих пор нет интерфейса. Часть, которая вас действительно интересует для модульного тестирования или даже для разработки ваших сервисов, - это HttpMessageHandler, поскольку именно она возвращает ответ, и вы можете поиздеваться над этим.

Стоит также отметить, что вы, вероятно, должны начать относиться к HttpClient как к более крупной сделке. Например: сведите к минимуму количество новых клиентов Http. Повторно используйте их, они предназначены для повторного использования и используют дерьмо меньше ресурсов, если вы делаете. Если вы начнете относиться к нему как к более крупной сделке, то будете чувствовать себя намного более неправым, пытаясь его высмеять, и теперь обработчик сообщений станет тем, что вы вводите, а не клиентом.

Другими словами, проектируйте свои зависимости вокруг обработчика вместо клиента. Более того, абстрактные "сервисы", использующие HttpClient, которые позволяют вам вводить обработчик, и вместо этого использовать его в качестве инъецируемой зависимости. Затем в ваших тестах вы можете подделать обработчик, чтобы контролировать ответ для настройки ваших тестов.

Обертывание HttpClient - безумная трата времени.

Обновление: см. Пример Джошуа Думса. Это именно то, что я рекомендую.

Ответ 5

Как уже упоминалось в комментариях, вам нужно abstract от 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();  
}

Ответ 6

Основываясь на других ответах, я предлагаю этот код, который не имеет внешних зависимостей:

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

Ответ 7

Я думаю, проблема в том, что у вас есть это немного вверх дном.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Если вы посмотрите на класс выше, я думаю, что это то, что вы хотите. Корпорация Майкрософт рекомендует поддерживать работоспособность клиента для оптимальной производительности, поэтому этот тип структуры позволяет вам это сделать. Также HttpMessageHandler является абстрактным классом и поэтому макет. Тогда ваш тестовый метод будет выглядеть следующим образом:

[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 auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
        // Act
        // Assert
    }

Ответ 8

Один из моих коллег заметил, что большинство методов HttpClient все они вызывают SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) под капотом, который является виртуальным методом вне HttpMessageInvoker:

Таким образом, на сегодняшний день самый простой способ HttpClient - просто HttpClient этот конкретный метод:

var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);

и ваш код может вызывать большинство (но не все) HttpClient класса HttpClient, включая обычный

httpClient.SendAsync(req)

Нажмите здесь, чтобы подтвердить https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

Ответ 9

Одной из альтернатив может быть настройка тупого HTTP-сервера, который возвращает готовые ответы на основе шаблона, соответствующего URL-адресу запроса, то есть вы тестируете реальные HTTP-запросы, а не фиктивные. Исторически это требовало значительных усилий по развитию и было бы слишком медленным, чтобы рассматривать его для модульного тестирования, однако библиотека OSS WireMock.net проста в использовании и достаточно быстра, чтобы запускаться с большим количеством тестов, поэтому стоит подумать. Настройка состоит из нескольких строк кода:

var server = FluentMockServer.Start();
server.Given(
      Request.Create()
      .WithPath("/some/thing").UsingGet()
   )
   .RespondWith(
       Response.Create()
       .WithStatusCode(200)
       .WithHeader("Content-Type", "application/json")
       .WithBody("{'attr':'value'}")
   );

Вы можете найти более подробную информацию и руководство по использованию wiremock в тестах здесь.

Ответ 10

Вступление в партию немного запоздало, но мне нравится, когда это возможно, использовать проводное соединение (https://github.com/WireMock-Net/WireMock.Net) в тестировании интеграции микросервиса ядра dotnet с нижестоящими зависимостями REST.

Реализуя TestHttpClientFactory, расширяющий IHttpClientFactory, мы можем переопределить метод

HttpClient CreateClient (имя строки)

Таким образом, при использовании именованных клиентов в вашем приложении вы управляете возвратом HttpClient, подключенного к вашей проводке.

Хорошая вещь в этом подходе состоит в том, что вы ничего не меняете в тестируемом приложении и позволяете тестам интеграции курса делать фактический запрос REST к вашему сервису и высмеивать json (или любой другой), который должен возвращать фактический нисходящий запрос. Это приводит к кратким тестам и как можно меньше насмешек в вашем приложении.

    public class TestHttpClientFactory : IHttpClientFactory 
{
    public HttpClient CreateClient(string name)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
            // G.Config is our singleton config access, so the endpoint 
            // to the running wiremock is used in the test
        };
        return httpClient;
    }
}

а также

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);

Ответ 11

Вы могли бы использовать библиотеку RichardSzalay MockHttp, которая макетирует HttpMessageHandler и может возвращать объект HttpClient, который будет использоваться во время тестов.

GitHub MockHttp

PM> Install-Package RichardSzalay.MockHttp

Из документации GitHub

MockHttp определяет замену HttpMessageHandler, движок, который управляет HttpClient, который обеспечивает свободный API конфигурации и обеспечивает постоянный ответ. Вызывающая сторона (например, уровень обслуживания вашего приложения) не знает о его присутствии.

Пример из GitHub

 var mockHttp = new MockHttpMessageHandler();

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

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

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

Ответ 12

Это старый вопрос, но я чувствую желание расширить ответы решением, которого я не видел здесь.
Вы можете подделать Microsoft в сборе (System.Net.Http), а затем использовать ShinsContext во время теста.

  1. В VS 2017 щелкните правой кнопкой мыши сборку System.Net.Http и выберите "Добавить сборку подделок"
  2. Поместите свой код в метод модульного теста в ShimsContext.Create(), используя. Таким образом, вы можете изолировать код, в котором вы планируете подделать HttpClient.
  3. В зависимости от вашей реализации и тестирования, я бы предложил реализовать все желаемые действия, когда вы вызываете метод в HttpClient и хотите подделать возвращаемое значение. Использование ShimHttpClient.AllInstances подделает вашу реализацию во всех экземплярах, созданных во время теста. Например, если вы хотите подделать метод GetAsync(), сделайте следующее:

    [TestMethod]
    public void FakeHttpClient()
    {
        using (ShimsContext.Create())
        {
            System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) =>
            {
              //Return a service unavailable response
              var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
              var task = Task.FromResult(httpResponseMessage);
              return task;
            };
    
            //your implementation will use the fake method(s) automatically
            var client = new Connection(_httpClient);
            client.doSomething(); 
        }
    }
    

Ответ 13

Я сделал что-то очень простое, как я был в окружении DI.

public class HttpHelper : IHttpHelper
{
    private ILogHelper _logHelper;

    public HttpHelper(ILogHelper logHelper)
    {
        _logHelper = logHelper;
    }

    public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
    {
        HttpResponseMessage response;
        using (var client = new HttpClient())
        {
            if (headers != null)
            {
                foreach (var h in headers)
                {
                    client.DefaultRequestHeaders.Add(h.Key, h.Value);
                }
            }
            response = await client.GetAsync(uri);
        }

        return response;
    }

    public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
    {
        ...

        rawResponse = await GetAsync(uri, headers);

        ...
    }

}

и макет это:

    [TestInitialize]
    public void Initialize()
    {
       ...
        _httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
       ...
    }

    [TestMethod]
    public async Task SuccessStatusCode_WithAuthHeader()
    {
        ...

        _httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
            Task<HttpResponseMessage>.Factory.StartNew(() =>
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(_testData))
                };
            })
        );
        var result = await _httpHelper.Object.GetAsync<TestDTO>(...);

        Assert.AreEqual(...);
    }

Ответ 14

Меня не убеждают многие ответы.

Прежде всего, представьте, что вы хотите выполнить модульное тестирование метода, использующего HttpClient. Вы не должны создавать экземпляр HttpClient непосредственно в вашей реализации. Вы должны внедрить на фабрике ответственность за предоставление вам экземпляра HttpClient. Таким образом, вы можете позже посмеяться над этой фабрикой и вернуть тот HttpClient вам нужен (например, фиктивный HttpClient а не реальный).

Итак, у вас будет фабрика, подобная следующей:

public interface IHttpClientFactory
{
    HttpClient Create();
}

И реализация:

public class HttpClientFactory
    : IHttpClientFactory
{
    public HttpClient Create()
    {
        var httpClient = new HttpClient();
        return httpClient;
    }
}

Конечно, вам нужно зарегистрировать в вашем контейнере IoC эту реализацию. Если вы используете Autofac, это будет что-то вроде:

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();

Теперь у вас будет правильная и проверяемая реализация. Представьте, что ваш метод похож на:

public class MyHttpClient
    : IMyHttpClient
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<string> PostAsync(Uri uri, string content)
    {
        using (var client = _httpClientFactory.Create())
        {
            var clientAddress = uri.GetLeftPart(UriPartial.Authority);
            client.BaseAddress = new Uri(clientAddress);
            var content = new StringContent(content, Encoding.UTF8, "application/json");
            var uriAbsolutePath = uri.AbsolutePath;
            var response = await client.PostAsync(uriAbsolutePath, content);
            var responseJson = response.Content.ReadAsStringAsync().Result;
            return responseJson;
        }
    }
}

Сейчас часть тестирования. HttpClient расширяет HttpMessageHandler, который является абстрактным. Давайте создадим "макет" HttpMessageHandler который принимает делегата, поэтому, когда мы используем макет, мы также можем настроить каждое поведение для каждого теста.

public class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

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

И теперь, и с помощью Moq (и FluentAssertions, библиотеки, которая делает модульные тесты более читабельными), у нас есть все необходимое для модульного тестирования нашего метода PostAsync, который использует HttpClient

public static class PostAsyncTests
{
    public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
        : Given_WhenAsync_Then_Test
    {
        private SalesOrderHttpClient _sut;
        private Uri _uri;
        private string _content;
        private string _expectedResult;
        private string _result;

        protected override void Given()
        {
            _uri = new Uri("http://test.com/api/resources");
            _content = "{\"foo\": \"bar\"}";
            _expectedResult = "{\"result\": \"ok\"}";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var messageHandlerMock =
                new MockHttpMessageHandler((request, cancellation) =>
                {
                    var responseMessage =
                        new HttpResponseMessage(HttpStatusCode.Created)
                        {
                            Content = new StringContent("{\"result\": \"ok\"}")
                        };

                    var result = Task.FromResult(responseMessage);
                    return result;
                });

            var httpClient = new HttpClient(messageHandlerMock);
            httpClientFactoryMock
                .Setup(x => x.Create())
                .Returns(httpClient);

            var httpClientFactory = httpClientFactoryMock.Object;

            _sut = new SalesOrderHttpClient(httpClientFactory);
        }

        protected override async Task WhenAsync()
        {
            _result = await _sut.PostAsync(_uri, _content);
        }


        [Fact]
        public void Then_It_Should_Return_A_Valid_JsonMessage()
        {
            _result.Should().BeEquivalentTo(_expectedResult);
        }
    }
}

Очевидно, что это глупый тест, и мы действительно тестируем наш макет. Но ты получил идею. Вы должны проверить значимую логику в зависимости от вашей реализации, например..

  • если код статуса ответа не 201, должен ли он генерировать исключение?
  • если текст ответа не может быть проанализирован, что должно произойти?
  • и т.п.

Цель этого ответа состояла в том, чтобы протестировать что-то, использующее HttpClient, и это хороший чистый способ сделать это.

Ответ 15

Все, что вам нужно, это тестовая версия класса HttpMessageHandler которую вы передаете в HttpClient ctor. Суть в том, что ваш тестовый класс HttpMessageHandler будет иметь делегат HttpRequestHandler который вызывающие абоненты могут устанавливать и просто обрабатывать HttpRequest так, как они хотят.

public class FakeHttpMessageHandler : HttpMessageHandler
    {
        public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
        (r, c) => 
            new HttpResponseMessage
            {
                ReasonPhrase = r.RequestUri.AbsoluteUri,
                StatusCode = HttpStatusCode.OK
            };


        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(HttpRequestHandler(request, cancellationToken));
        }
    }

Вы можете использовать экземпляр этого класса для создания конкретного экземпляра HttpClient. Посредством делегата HttpRequestHandler вы получаете полный контроль над исходящими http-запросами от HttpClient.

Ответ 16

Вдохновленный ответом PointZeroTwo, вот пример использования NUnit и FakeItEasy.

SystemUnderTest в этом примере - это класс, который вы хотите протестировать - для него не дано никакого образца контента, но я предполагаю, что у вас это уже есть!

[TestFixture]
public class HttpClientTests
{
    private ISystemUnderTest _systemUnderTest;
    private HttpMessageHandler _mockMessageHandler;

    [SetUp]
    public void Setup()
    {
        _mockMessageHandler = A.Fake<HttpMessageHandler>();
        var httpClient = new HttpClient(_mockMessageHandler);

        _systemUnderTest = new SystemUnderTest(httpClient);
    }

    [Test]
    public void HttpError()
    {
        // Arrange
        A.CallTo(_mockMessageHandler)
            .Where(x => x.Method.Name == "SendAsync")
            .WithReturnType<Task<HttpResponseMessage>>()
            .Returns(Task.FromResult(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.InternalServerError,
                Content = new StringContent("abcd")
            }));

        // Act
        var result = _systemUnderTest.DoSomething();

        // Assert
        // Assert.AreEqual(...);
    }
}

Ответ 17

Здесь простое решение, которое хорошо сработало для меня.

Использование библиотеки moq mocking.

// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
   .Protected()
   // Setup the PROTECTED method to mock
   .Setup<Task<HttpResponseMessage>>(
      "SendAsync",
      ItExpr.IsAny<HttpRequestMessage>(),
      ItExpr.IsAny<CancellationToken>()
   )
   // prepare the expected response of the mocked http call
   .ReturnsAsync(new HttpResponseMessage()
   {
      StatusCode = HttpStatusCode.OK,
      Content = new StringContent("[{'id':1,'value':'1'}]"),
   })
   .Verifiable();

// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
   BaseAddress = new Uri("http://test.com/"),
};

var subjectUnderTest = new MyTestClass(httpClient);

// ACT
var result = await subjectUnderTest
   .GetSomethingRemoteAsync('api/test/whatever');

// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);

// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");

handlerMock.Protected().Verify(
   "SendAsync",
   Times.Exactly(1), // we expected a single external request
   ItExpr.Is<HttpRequestMessage>(req =>
      req.Method == HttpMethod.Get  // we expected a GET request
      && req.RequestUri == expectedUri // to this uri
   ),
   ItExpr.IsAny<CancellationToken>()
);

Источник: https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/