Издевательство над 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;
    }
}

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

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

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

26 ответов

Решение

Ваш интерфейс выставляет конкретный 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 (См . Пример Нкоси, это гораздо лучший подход, чем мой). Жизнь была бы намного проще, если 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'}

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

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

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

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

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

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

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

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

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

Существует несколько различных подходов к имитации файла . Вот несколько примеров использования POC.xUnitпрежде чем принять решение об одном решении (). Обратите внимание, что у каждого фреймворка гораздо больше возможностей, чем показано ниже; Я сделал каждый пример кратким для ясности.

Мок (сам по себе)

Это относительно просто, если вы знакомы с использованием фреймворка . «Фишка» состоит в том, чтобы издеваться надHttpMessageHandlerвнутри - не то само. ПРИМЕЧАНИЕ. Рекомендуется использоватьMockBehavior.Strictв макете, чтобы вы были предупреждены о любых вызовах, которые вы явно не имитировали и ожидали.

RichardSzalay.MockHttp

RichardSzalay.MockHttp — еще одно популярное решение. Я использовал это в прошлом, но нашел это немного более громоздким, чемMoq.Contrib.HttpClient. Здесь можно использовать два разных шаблона. Ричард описывает, когда использовать один против другого здесь .

Moq.Contrib.HttpClient

Как и решение для самостоятельного использования, это просто, если вы знакомы с использованием фреймворка MoqMoq . Я нашел это решение немного более прямым с меньшим количеством кода. Это решение, которое я решил использовать. Обратите внимание, что для этого решения требуется отдельный Nuget отMoqсам - Moq.Contrib.HttpClient

WireMock.Net

Новичок в игре WireMock.net набирает популярность. Это было бы разумным решением вместоMicrosoft.AspNetCore.TestHostесли вы пишете интеграционные тесты, в которых вызовы конечной точки фактически выполняются, а не имитируются. Сначала я думал, что это будет мой выбор, но отказался от него по двум причинам:

  1. он действительно открывает порты для облегчения теста. Поскольку мне приходилось исправлять проблемы с исчерпанием портов из-за неправильного использованияHttpClientв прошлом я решил отказаться от этого решения, так как не был уверен, насколько хорошо оно будет масштабироваться в большой кодовой базе с множеством параллельных модульных тестов.
  2. Используемые URL-адреса должны быть разрешимыми (фактические законные URL-адреса). Если вы хотите простоты, не заботясь о «настоящем» URL-адресе (только о том, что URL-адрес, который вы ожидали, был фактически вызван), то это может быть не для вас.

Пример

Учитывая следующий упрощенный/надуманный код, вот как вы будете писать каждый тест.

      public class ClassUnderTest
{
    private readonly HttpClient _httpClient;
    private const string Url = "https://myurl";

    public ClassUnderTest(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Person> GetPersonAsync(int id)
    {
        var response = await _httpClient.GetAsync($"{Url}?id={id}");
        return await response.Content.ReadFromJsonAsync<Person>();
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

Мок (сам по себе)

      [Fact]
public async Task JustMoq()
{
    //arrange
    const int personId = 1;
    var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    mockHandler
        .Protected()
        .Setup<Task<HttpResponseMessage>>(
            "SendAsync",
            ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
            ItExpr.IsAny<CancellationToken>())
        .ReturnsAsync(mockResponse);

    // Inject the handler or client into your application code
    var httpClient = new HttpClient(mockHandler.Object);
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    mockHandler.Protected().Verify(
        "SendAsync",
        Times.Exactly(1),
        ItExpr.Is<HttpRequestMessage>(m => m.Method == HttpMethod.Get),
        ItExpr.IsAny<CancellationToken>());
}

RichardSzalay.MockHttp (используя шаблон BackendDefinition)

      [Fact]
public async Task RichardSzalayMockHttpUsingBackendDefinition()
{
    //arrange
    const int personId = 1;
    using var mockHandler = new MockHttpMessageHandler();
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    var mockedRequest = mockHandler.When(HttpMethod.Get, "https://myurl?id=1")
        .Respond(mockResponse.StatusCode, mockResponse.Content);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.ToHttpClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
    mockHandler.VerifyNoOutstandingRequest();
}

RichardSzalay.MockHttp (с использованием шаблона RequestExpectation)

      [Fact]
public async Task RichardSzalayMockHttpUsingRequestExpectation()
{
    //arrange
    const int personId = 1;
    using var mockHandler = new MockHttpMessageHandler();
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockResponse = new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = JsonContent.Create<Person>(dto)
    };

    var mockedRequest = mockHandler.Expect(HttpMethod.Get, "https://myurl")
        .WithExactQueryString($"id={personId}")
        .Respond(mockResponse.StatusCode, mockResponse.Content);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.ToHttpClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    Assert.Equal(1, mockHandler.GetMatchCount(mockedRequest));
    mockHandler.VerifyNoOutstandingExpectation();
}

Moq.Contrib.HttpClient

      [Fact]
public async Task UsingMoqContribHttpClient()
{
    //arrange
    const int personId = 1;
    var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
    var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
    var mockUrl = $"https://myurl?id={personId}";
    var mockResponse = mockHandler.SetupRequest(HttpMethod.Get, mockUrl)
        .ReturnsJsonResponse<Person>(HttpStatusCode.OK, dto);

    // Inject the handler or client into your application code
    var httpClient = mockHandler.CreateClient();
    var sut = new ClassUnderTest(httpClient);

    //act
    var actual = await sut.GetPersonAsync(personId);

    //assert
    Assert.NotNull(actual);
    Assert.Equivalent(dto, actual);
    mockHandler.VerifyRequest(HttpMethod.Get, mockUrl, Times.Once());
}

WireMock.NET

      public class TestClass : IDisposable
{
    private WireMockServer _server;

    public TestClass()
    {
        _server = WireMockServer.Start();
    }

    public void Dispose()
    {
        _server.Stop();
    }

    [Fact]
    public async Task UsingWireMock()
    {
        //arrange
        const int personId = 1;
        var dto = new Person { Id = personId, Name = "Dave", Age = 42 };
        var mockUrl = $"https://myurl?id={personId}";

        _server.Given(
            Request.Create()
                .WithPath("/"))
            .RespondWith(
                Response.Create()
                    .WithStatusCode(200)
                    .WithHeader("Content-Type", "application/json")
                    .WithBodyAsJson(dto));

        // Inject the handler or client into your application code
        var httpClient = _server.CreateClient();
        var sut = new ClassUnderTest(httpClient);

        //act
        var actual = await sut.GetPersonAsync(personId);

        //assert
        Assert.NotNull(actual);
        Assert.Equivalent(dto, actual);
    }
}

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

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

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

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

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 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 в тестах здесь.

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

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

Microsoft теперь рекомендует использовать IHttpClientFactory вместо прямого использования HttpClient:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0

Пример макета с запросом, возвращающим ожидаемый результат:

      private LoginController GetLoginController()
{
    var expected = "Hello world";
    var mockFactory = new Mock<IHttpClientFactory>();

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

    var httpClient = new HttpClient(mockMessageHandler.Object);

    mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);

    var logger = Mock.Of<ILogger<LoginController>>();

    var controller = new LoginController(logger, mockFactory.Object);

    return controller;
}

Источник:

/questions/41820628/kak-smodelirovat-novyij-httpclientfactory-vnet-core-21-ispolzuya-moq/56769735#56769735

НЕ ИМЕЙТЕ оболочки, которая создает новый экземпляр HttpClient. Если вы это сделаете, во время выполнения у вас закончатся сокеты (даже если вы удаляете объект HttpClient).

Если вы используете MOQ, правильный способ сделать это - добавить using Moq.Protected; к вашему тесту, а затем напишите код, подобный следующему:

      var response = new HttpResponseMessage(HttpStatusCode.OK)
{
    Content = new StringContent("It worked!")
};
var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
mockHttpMessageHandler
    .Protected()
    .Setup<Task<HttpResponseMessage>>(
        "SendAsync",
        ItExpr.IsAny<HttpRequestMessage>(),
        ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(() => response);


var httpClient = new HttpClient(mockHttpMessageHandler.Object);

Вступление в партию немного запоздало, но мне нравится, когда это возможно, использовать проводное соединение ( 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);

Если вы не против запустить собственный http-сервер, вы можете попробовать Xim. Это так просто:

      using Xim.Simulators.Api;
      [Test]
public async Task TestHttpGetMethod()
{
    using var simulation = Simulation.Create();
    using var api = simulation
        .AddApi()
        .AddHandler("GET /books/1234", ApiResponse.Ok())
        .Build();
    await api.StartAsync();
    var httpClient = new HttpClient();

    var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{api.Location}/books/1234"));

    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    Assert.IsTrue(api.ReceivedApiCalls.Any(call => call.Action == "GET /books/1234"));
}

Это прекрасная альтернатива использованию моков, которая может удовлетворить ваши потребности в некоторых сценариях. Он построен на Kestrel (и да, я автор).

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

    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.

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

Вы могли бы использовать библиотеку 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 в сборе (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(); 
        }
    }
    

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

Для создания этой упаковки HttpClient к 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 с key как Uri а также value как 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издевательским фреймворком или как показано ниже. Эта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 реализация будет соответствовать Uri в 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: есть образец реализации

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

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

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

Чтобы добавить свои 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();

Я пробовал много из этих предложений в своих модульных тестах - самым простым и быстрым для настройки и начала работы было:

Moq.Contrib.HttpClient

Приведенный выше ответ с защищенным не работает, вы получите ошибку в параметрах SendAsync, которые не совпадают.

Другие вопросы по тегам