Знущання з HttpClient в одиничних тестах


110

У мене є проблеми, які намагаються обернути свій код, який буде використовуватися в одиничних тестах. Питання в цьому. У мене є інтерфейс 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. Як слід підходити до цієї проблеми?

Заздалегідь спасибі.


1
Розкриття HttpClientінтерфейсу у вашому інтерфейсі - це проблема. Ви змушуєте свого клієнта використовувати HttpClientконкретний клас. Замість цього, ви повинні викрити абстракції з HttpClient.
Майк Ейсон

Чи можете ви пояснити це дещо глибше? Як мені створити конструктор класів з'єднань, оскільки я не хочу, щоб в інших класах були залежності HttpClient, використовуючи клас Connection. Наприклад, я не хочу передавати concerete HttpClient в конструктор Connection, тому що це зробить кожен інший клас, який використовує Connection, залежним від HttpClient?
тюгг

З інтересу, що ви гуглили? Мабуть, 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(див , наприклад НКОЗ в , це набагато краще підхід , ніж у мене). Життя було б набагато простішим, якби HttpClientбув інтерфейс, над яким можна було знущатися, на жаль, це не так.

Цей приклад не є золотим квитком. IHttpHandlerяк і раніше покладається на те HttpResponseMessage, що належить до System.Net.Httpпростору імен, тому якщо вам потрібні інші реалізації, крім того HttpClient, вам доведеться виконати якесь відображення, щоб перетворити їх відповіді в HttpResponseMessageоб'єкти. Це, звичайно, тільки проблема , якщо вам потрібно використовувати декілька реалізацій з IHttpHandlerале це не виглядає , як ви робите так , що це не кінець світу, але це що - то думати.

У будь-якому випадку, ви можете просто знущатися, IHttpHandlerне турбуючись про конкретний HttpClientклас, оскільки він був абстрагований.

Я рекомендую протестувати неасинхронні методи, оскільки вони все ще називають асинхронні методи, але без клопоту не турбуватися про те, що асинхронні методи тестування одиниць дивіться тут


Це справді відповідає на моє запитання. Відповідь Нкосіса також правильна, тому я не впевнений, що мені слід прийняти як відповідь, але я піду з цією. Дякую обом за докладені зусилля
тюгг

@tjugg Радий допомогти. Не соромтеся голосувати за відповіді, якщо ви вважаєте їх корисними.
Nkosi

3
Варто зазначити, що головна відмінність цієї відповіді від Нкосі полягає в тому, що це набагато тонша абстракція. Тонкий, мабуть, хороший для скромного предмета
Бен Аронсон

227

Розширюваність 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 Meshandler? Або як ви реалізували це у власних проектах
тюгг

2
Чудова відповідь і те, що я б не знав спочатку. Робить роботу з HttpClient не так вже й погано.
Білер

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

3
@ChrisMarisic ви пропонуєте форму розташування служби для заміни ін'єкції. Місце обслуговування - добре відомий антидіапазон, тому введення імхо є кращим.
MarioDS

2
@MarioDS і незалежно, ви взагалі не повинні вводити екземпляр HttpClient . Якщо ви готові використовувати конструктор для ін'єкцій для цього, вам слід вводити HttpClientFactoryяк в Func<HttpClient>. Враховуючи, що я розглядаю HttpClient як суто детальну інформацію про реалізацію, а не залежність, я буду використовувати статику так само, як я проілюстрував вище. Я цілком в порядку з тестами, що маніпулюють внутрішніми. Якщо я дбаю про чистого вигляду, я буду стояти на повному сервері і тестувати контури в реальному часі. Використання будь-якого типу глузування означає, що ви приймаєте наближення поведінки, а не фактичну поведінку.
Кріс Марісіч

39

Я погоджуюся з деякими іншими відповідями, що найкращим підходом є глузування з 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()Був вбивцею. Дякую за цей приклад. Це дозволяє написати тест, не змінюючи джерела взагалі.
тиріон

1
FYI, 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 (). Перевірте (...), але схоже, що це не працює з методами асинхронізації.
Ророман

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. Але я просто не вибираю, як ви можете замінити корпус інтерфейсу обробника членами абстрагованого клієнта.

Потім реалізація IHttpClientcan може бути використана для обгортання / адаптації реального / конкретного HttpClientабо будь-якого іншого об'єкта з цього питання, який можна використовувати для створення HTTP-запитів, як того, що ви дійсно хотіли, - це сервіс, який надав цю функціональність як призначенийHttpClient конкретно призначено. Використання абстракції - це чистий (на мою думку) і підхід SOLID, і може зробити ваш код більш ретельним, якщо вам потрібно буде вимкнути базовий клієнт для чогось іншого, оскільки зміни фреймворка.

Ось фрагмент того, як можна зробити реалізацію.

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

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

    //...other code

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

    //...other code
}

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

Ви, наприклад, клас підключення можна вводити разом із клієнтом, що абстрагується

public class Connection
{
    private IHttpClient _httpClient;

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

Потім ваш тест може знущатися з того, що потрібно для вашого SUT

private IHttpClient _httpClient;

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

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

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

ЦЕ відповідь. Абстракція вище 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'}")
   );

Більш детальну інформацію та вказівки щодо використання проводів в тестах можна знайти тут.


8

Ось просте рішення, яке добре спрацювало для мене.

Використання moq глузливої ​​бібліотеки.

// 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/


Я також це успішно використовував. Я віддаю перевагу цьому, аніж дроггінг у ще більшої нульової залежності, і ви насправді дізнаєтесь трохи більше про те, що відбувається під капотом. Приємно, що більшість методів SendAsyncвсе-таки використовуються, тому не потрібні додаткові налаштування.
Стів Петтіфер

4

Я не переконаний у багатьох відповідях.

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

Отже, у вас є завод на зразок наступного:

public interface IHttpClientFactory
{
    HttpClient Create();
}

І реалізація:

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

Звичайно, вам потрібно буде зареєструвати у вашому IoC Container цю реалізацію. Якщо ви використовуєте 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, і це приємний чистий спосіб зробити це.


4

Приєднуйтесь до вечірки трохи пізно, але мені подобається користуватися дротяним способом ( https://github.com/WireMock-Net/WireMock.Net ), коли це можливо, для інтеграції тесту основних мікросервісів дотнет-ядра із залежними від 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);

2

Оскільки 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метод, для цього створити a 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 Посилання: яка має зразок реалізації


Хоча цей код може вирішити питання, включаючи пояснення, як і чому це вирішує проблему, справді допоможе покращити якість вашої публікації та, ймовірно, призведе до збільшення кількості голосів. Пам'ятайте, що ви відповідаєте на запитання читачів у майбутньому, а не лише про людину, яка зараз задає питання. Будь ласка, відредагуйте свою відповідь, щоб додати пояснення та вказати, які обмеження та припущення застосовуються.
Богдан Опір

Дякую @ БогданОпир за відгуки оновлених пояснень.
ghosh-arun

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.AllInsances підробить вашу реалізацію у всіх екземплярах, створених під час вашого тесту. Наприклад, якщо ви хочете підробити метод 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класу, яку ви 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 ви маєте повний контроль над вихідними HT-запитами від 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, будь ласка, Отримати або Опублікувати. Це працювало для мене.

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.