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


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

public interface IHttpHandler
{
    HttpClient client { get; }
}

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

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

а затем класс соединения, который использует 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?     

}

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

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

14 56

14 ответов:

ваш интерфейс выставляет бетон 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.

Если вы предпочитаете 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'}

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

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

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

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

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

обновление: Пример см. Иисус Навин обрекает это. Это именно то, что я рекомендую.

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

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

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

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

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

один из моих коллег заметил, что большинство 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

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

вы можете использовать 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'}

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

  1. в VS 2017, щелкните правой кнопкой мыши на сборке System.Net.Http и выберите "Добавить сборку подделок"
  2. поместите свой код в метод модульного теста под ShimsContext.Создать () с помощью. Таким образом, вы можете изолировать код, где вы планируете фейк 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(); 
        }
    }
    

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

я не убежден во многих ответах.

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

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, должен ли он вызвать исключение?
  • если текст ответа не может быть проанализирован, что должно произойти?
  • etc.

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

все, что вам нужно, это тестовая версия 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.