Мокинг HttpClient в модульных тестах


111

У меня есть некоторые проблемы с попыткой обернуть мой код для использования в модульных тестах. Проблема вот в чем. Имею интерфейс 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;
    }
}

И затем у меня есть проект модульного тестирования, в котором есть этот класс:

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. Как мне подойти к этой проблеме?

Заранее спасибо.


1
HttpClientПроблема заключается в выставлении a в вашем интерфейсе. Вы заставляете своего клиента использовать HttpClientконкретный класс. Вместо этого, вы должны разоблачить абстракции из HttpClient.
Майк Исон

Можете ли вы объяснить это немного подробнее? Как мне создать конструктор классов подключения, потому что я не хочу, чтобы какие-либо зависимости HttpClient в других классах использовали класс Connection. Например, я не хочу передавать Concerete HttpClient в конструктор Connection, потому что это сделало бы любой другой класс, использующий Connection, зависимым от HttpClient?
tjugg

Из интереса, что вы гуглили? Очевидно, mockhttp может использовать некоторые улучшения SEO.
Ричард Салай

@Mike - как уже упоминалось в моем ответе, на самом деле нет необходимости абстрагироваться от HttpClient. Это прекрасно тестируется как есть. У меня есть множество проектов, в которых есть наборы тестов без серверной части, использующие этот метод.
Ричард Салай

Ответы:


37

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

Я рекомендую тестировать неасинхронные методы, так как они по-прежнему вызывают асинхронные методы, но без проблем, связанных с асинхронными методами модульного тестирования, см. Здесь


Это действительно ответ на мой вопрос. Ответ Nkosis также верен, поэтому я не уверен, что мне следует принять в качестве ответа, но я пойду с этим. Спасибо вам обоим за усилия
tjugg

@tjugg Рад помочь. Не стесняйтесь голосовать за ответы, если вы сочли их полезными.
Nkosi

3
Стоит отметить, что основное различие между этим ответом и ответом Нкози в том, что это гораздо более тонкая абстракция. Тонкий, вероятно, хорош для скромного объекта,
Бен Ааронсон

228

Расширяемость 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'}

1
Итак, я бы просто передал MockHttpMessageHandler в качестве класса Httphandler обработчика сообщений? Или как вы реализовали это в своих проектах
tjugg

2
Отличный ответ и то, чего я изначально не знал. Делает работу с HttpClient не такой уж плохой.
Bealer

6
Для людей, которые не хотят иметь дело с инъекцией клиента, но все же хотят легкой тестируемости, это тривиально. Просто замените var client = new HttpClient()с var client = ClientFactory()и настройка поля internal static Func<HttpClient> ClientFactory = () => new HttpClient();и на уровне теста вы можете переписать это поле.
Крис Марисич

3
@ChrisMarisic вы предлагаете форму размещения службы для замены инъекции. Местоположение службы - это хорошо известный анти-шаблон, поэтому предпочтительнее внедрение imho.
MarioDS

2
@MarioDS и, тем не менее, вам вообще не следует вводить экземпляр HttpClient . Если вы категорически настроены на использование для этого конструктора инъекции, вам следует ввести HttpClientFactoryкак в Func<HttpClient>. Поскольку я рассматриваю HttpClient исключительно как деталь реализации, а не как зависимость, я буду использовать статику, как показано выше. Меня полностью устраивают тесты, управляющие внутренними устройствами. Если меня волнует чистый изм, я выставлю полные серверы и протестирую пути к живому коду. Использование любых имитаций означает, что вы принимаете приблизительное поведение, а не реальное поведение.
Крис Марисич

40

Я согласен с некоторыми другими ответами, что лучший подход - издеваться над 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);
        }
    }
}

1
Мне это очень понравилось. Это mockMessageHandler.Protected()был убийца. Спасибо за этот пример. Это позволяет писать тест, вообще не изменяя исходный код.
Tyrion

1
К вашему сведению, Moq 4.8 поддерживает строго типизированное издевательство над защищенными участниками - github.com/Moq/moq4/wiki/Quickstart
Ричард Салай

2
Выглядит отлично. Также Moq поддерживает ReturnsAsync, поэтому код будет выглядеть так.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord

Спасибо @kord, я добавил это к ответу
PointZeroTwo

3
Есть ли способ проверить, что SandAsync был вызван с некоторыми параметрами? Я пробовал использовать ... Protected (). Verify (...), но похоже, что он не работает с асинхронными методами.
Rroman

29

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

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

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

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

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

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


17

Как также упоминалось в комментариях, вам нужно абстрагироваться от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конкретно. Использование абстракции - это чистый (Мое мнение) и твердый подход, который может сделать ваш код более удобным в обслуживании, если вам нужно переключить базовый клиент на что-то еще по мере изменения структуры.

Вот фрагмент того, как можно реализовать реализацию.

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

ЭТО и есть ответ. Вышеупомянутая абстракция HttpClientи адаптер для создания вашего конкретного экземпляра с использованием HttpClientFactory. Это делает тестирование логики, выходящей за рамки HTTP-запроса, тривиальным, что и является нашей целью.
pimbrouwers

13

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

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

4
Вы эффективно тестируете свой макет. Настоящая сила макета в том, что вы можете устанавливать ожидания и изменять его поведение в каждом тесте. Тот факт, что вы должны реализовать некоторые из них HttpMessageHandlerсамостоятельно, делает это практически невозможным - и вы должны это делать, потому что методы есть protected internal.
MarioDS

3
@MarioDS Я думаю, дело в том, что вы можете имитировать HTTP-ответ, чтобы можно было протестировать остальной код. Если вы внедрите фабрику, которая получает HttpClient, то в тестах вы можете предоставить этот HttpClient.
chris31389

13

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

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Если вы посмотрите на класс выше, я думаю, что это то, что вам нужно. Microsoft рекомендует поддерживать работоспособность клиента для оптимальной производительности, поэтому такой тип структуры позволяет это делать. Кроме того, 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 client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}

9

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

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

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

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

httpClient.SendAsync(req)

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


1
Однако это не работает для любого кода, который вызывает SendAsync(HttpRequestMessage)напрямую. Если вы можете изменить свой код, чтобы не использовать эту удобную функцию, то имитация HttpClient напрямую путем переопределения SendAsyncна самом деле является самым чистым решением, которое я нашел.
Дилан Николсон

8

Альтернативой может быть установка 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 в тестах здесь.


8

Вот простое решение, которое мне понравилось.

Использование библиотеки 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/


Я также успешно использовал это. Я предпочитаю это, а не dfraging, но с другой зависимостью nuget, и вы также узнаете немного больше о том, что происходит под капотом. Приятно то, что большинство методов SendAsyncвсе равно используются, поэтому дополнительных настроек не требуется.
Стив Петтифер

4

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

Прежде всего, представьте, что вы хотите провести модульное тестирование метода, который использует HttpClient . Вы не должны создавать экземпляры HttpClientнепосредственно в своей реализации. Вы должны внедрить factory с ответственностью за предоставление вам экземпляра 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;
        }
    }
}

Теперь тестовая часть. HttpClientextends 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, и это хороший чистый способ сделать это.


4

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

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

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

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

Хорошая вещь в этом подходе заключается в том, что вы ничего не меняете в тестируемом приложении и разрешаете интеграционные тесты курса, выполняя фактический 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);

2

Поскольку HttpClientиспользовать SendAsyncметод для выполнения всего HTTP Requests, вы можете override SendAsyncметод и издеваться надHttpClient .

Для создания обертки HttpClientв a interface, как показано ниже

public interface IServiceHelper
{
    HttpClient GetClient();
}

Затем используйте выше interfaceдля внедрения зависимостей в свой сервис, образец ниже

public class SampleService
{
    private readonly IServiceHelper serviceHelper;

    public SampleService(IServiceHelper serviceHelper)
    {
        this.serviceHelper = serviceHelper;
    }

    public async Task<HttpResponseMessage> Get(int dummyParam)
    {
        try
        {
            var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
            var client = serviceHelper.GetClient();
            HttpResponseMessage response = await client.GetAsync(dummyUrl);               

            return response;
        }
        catch (Exception)
        {
            // log.
            throw;
        }
    }
}

Теперь в проекте модульного теста создайте вспомогательный класс для издевательства SendAsync. Это FakeHttpResponseHandlerкласс, inheriting DelegatingHandlerкоторый предоставит возможность переопределить SendAsyncметод. После переопределения SendAsyncметода необходимо настроить ответ для каждого HTTP Requestвызывающего SendAsyncметода, для этого создайте с Dictionaryпомощью keyas Uriи valueas, HttpResponseMessageчтобы всякий раз, когда есть HTTP Requestи если Uriсовпадения SendAsync, возвращали настроенный HttpResponseMessage.

public class FakeHttpResponseHandler : DelegatingHandler
{
    private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
    private readonly JavaScriptSerializer javaScriptSerializer;
    public FakeHttpResponseHandler()
    {
        fakeServiceResponse =  new Dictionary<Uri, HttpResponseMessage>();
        javaScriptSerializer =  new JavaScriptSerializer();
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
    {
        fakeServiceResponse.Remove(uri);
        fakeServiceResponse.Add(uri, httpResponseMessage);
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    /// <param name="requestParameter">Query string parameter.</param>
    public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
    {
        var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
        var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
        fakeServiceResponse.Remove(actualUri);
        fakeServiceResponse.Add(actualUri, httpResponseMessage);
    }

    // all method in HttpClient call use SendAsync method internally so we are overriding that method here.
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if(fakeServiceResponse.ContainsKey(request.RequestUri))
        {
            return Task.FromResult(fakeServiceResponse[request.RequestUri]);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            RequestMessage = request,
            Content = new StringContent("Not matching fake found")
        });
    }
}

Создайте новую реализацию с IServiceHelperпомощью mocking framework или как показано ниже. Этот FakeServiceHelperкласс мы можем использовать для внедрения FakeHttpResponseHandlerкласса, чтобы каждый раз, когда он был HttpClientсоздан, classон использовал FakeHttpResponseHandler classвместо фактической реализации.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

    public FakeServiceHelper(DelegatingHandler delegatingHandler)
    {
        this.delegatingHandler = delegatingHandler;
    }

    public HttpClient GetClient()
    {
        return new HttpClient(delegatingHandler);
    }
}

И в тесте настройте FakeHttpResponseHandler class, добавив Uriи ожидаемый HttpResponseMessage. Он Uriдолжен быть фактической serviceконечной точкой, Uriчтобы при overridden SendAsyncвызове метода из фактической serviceреализации он совпадал с Uriin Dictionaryи отвечал настроенным HttpResponseMessage. После настройки введите FakeHttpResponseHandler objectподдельную IServiceHelperреализацию. Затем вставьте в FakeServiceHelper classфактическую службу, которая заставит фактическую службу использовать override SendAsyncметод.

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

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

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

Ссылка на GitHub: есть образец реализации


Хотя этот код может решить вопрос, в том числе объяснение того, как и почему это решает проблему, действительно поможет улучшить качество вашего сообщения и, вероятно, приведет к большему количеству голосов за. Помните, что вы отвечаете на вопрос для будущих читателей, а не только для человека, который задает его сейчас. Пожалуйста , измените свой ответ , чтобы добавить объяснения и дать указание о том , что применять ограничения и допущения.
Богдан Опир

Спасибо @ БогданОпир за отзыв обновленного объяснения.
гхош-арун

1

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

GitHub MockHttp

PM> Установочный пакет 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'}

1

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

1

Я сделал что-то очень простое, поскольку был в среде 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(...);
    }

1

Все, что вам нужно, это тестовая версия HttpMessageHandlerкласса, которую вы передаете HttpClientctor. Главное, чтобы у вашего тестового 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.


1

Вдохновленный ответом 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(...);
    }
}

что, если я хочу передать параметр методу, указанному напротив "x.Method.Name" ..?
Шайлеш

0

Возможно, в вашем текущем проекте нужно будет изменить какой-то код, но для новых проектов вы обязательно должны рассмотреть возможность использования Flurl.

https://flurl.dev

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

На веб-сайте есть множество примеров кода, но вкратце вы используете это в своем коде.

Добавьте использование.

using Flurl;
using Flurl.Http;

Отправьте запрос на получение и прочтите ответ.

public async Task SendGetRequest()
{
   var response = await "https://example.com".GetAsync();
   // ...
}

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

using (var httpTest = new HttpTest())
{
   // Arrange
   httpTest.RespondWith("OK", 200);

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}

0

После тщательного поиска я нашел лучший способ добиться этого.

    private HttpResponseMessage response;

    [SetUp]
    public void Setup()
    {
        var handlerMock = new Mock<HttpMessageHandler>();

        handlerMock
           .Protected()
           .Setup<Task<HttpResponseMessage>>(
              "SendAsync",
              ItExpr.IsAny<HttpRequestMessage>(),
              ItExpr.IsAny<CancellationToken>())
           // This line will let you to change the response in each test method
           .ReturnsAsync(() => response);

        _httpClient = new HttpClient(handlerMock.Object);

        yourClinet = new YourClient( _httpClient);
    }

Как вы заметили, я использовал пакеты Moq и Moq.Protected.


0

Чтобы добавить свои 2 цента. Чтобы имитировать определенные методы HTTP-запроса, используйте Get или Post. Это сработало для меня.

mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
                                                .Returns(Task.FromResult(new HttpResponseMessage()
                                                {
                                                    StatusCode = HttpStatusCode.OK,
                                                    Content = new StringContent(""),
                                                })).Verifiable();
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.