Runtime: Простой способ издеваться над методом httpClient.GetAsync(..) для модульных тестов?

Созданный на 4 мая 2015  ·  157Комментарии  ·  Источник: dotnet/runtime

System.Net.Http теперь загружен в репозиторий :smile: :tada: :balloon:

Всякий раз, когда я использовал это в каком-либо сервисе, он отлично работает, но затрудняет модульное тестирование => мои модульные тесты на самом деле не хотят когда-либо достигать этой реальной конечной точки.

Много лет назад я спросил @davidfowl , что нам делать? Я надеюсь, что перефразирую и не перепутаю его, но он предположил, что мне нужно подделать обработчик сообщений (например, HttpClientHandler ), подключить его и т. д.

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

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

Есть ли более простой способ? Типа… разве мы не можем просто иметь интерфейс IHttpClient и внедрить его в наш сервис?

Design Discussion area-System.Net.Http test enhancement

Самый полезный комментарий

Привет @SidharthNabar - Спасибо, что прочитали мою проблему, я тоже очень ценю это.

Создайте новый класс обработчика

Вы только что ответили на мой вопрос :)

Это тоже большой танец, чтобы покачиваться, просто чтобы попросить мой настоящий код не _попадать в сеть_.

Я даже сделал репозиторий HttpClient.Helpers и пакет nuget... просто чтобы упростить тестирование! Сценарий похож на счастливый путь или исключение, созданное конечной точкой сети...

Вот в чем проблема -> можем ли мы не делать все это и ... вместо этого просто издеваться над методом?

Я попытаюсь объяснить с помощью кода ..

Цель: Скачать что-нибудь из интернета.

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

Давайте рассмотрим два примера:

Учитывая интерфейс (если он существует):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

Мой код теперь может выглядеть так...

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

и тестовый класс

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

Ура! Готово.

Хорошо, теперь с текущим способом...

  1. создать новый класс Handler - да, класс!
  2. Внедрить обработчик в сервис
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

Но боль в том, что теперь я должен сделать класс. Ты сделал мне больно.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

И этот класс начинается довольно просто. До тех пор, пока у меня не будет несколько GetAsync calls (поэтому мне нужно предоставить несколько экземпляров Handler?) или несколько httpClients в одной службе. Кроме того, мы Dispose обработчика или повторно используем его, если мы делаем несколько вызовов в одном и том же блоке логики (что является опцией ctor)?

например.

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

с этим можно сделать намного проще..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

чисто чисто чисто :)

Затем - есть следующий бит: возможность обнаружения

Когда я впервые начал использовать MS.Net.Http.HttpClient , API было довольно очевидным :+1: Хорошо - получить строку и сделать это асинхронно.. просто...

... но потом я попал на тестирование .... и теперь я должен узнать о HttpClientHandlers ? Эм почему? Я чувствую, что все это должно быть скрыто под обложками, и мне не нужно беспокоиться обо всех этих деталях реализации. Это слишком много! Вы говорите, что я должен начать заглядывать внутрь коробки и изучать кое-что из сантехники... что больно :cry:

Все это делает это более сложным, чем должно быть, ИМО.


Помоги мне Microsoft - ты моя единственная надежда.

Все 157 Комментарий

HttpClient — это не что иное, как слой абстракции над другой библиотекой HTTP. Одна из его основных целей — позволить вам заменить поведение (реализация), включая, помимо прочего, возможность создавать детерминированные модульные тесты.

В других работах HttpClient сам по себе служит как «настоящим», так и «фиктивным» объектом, а HttpMessageHandler — это то, что вы выбираете для удовлетворения потребностей своего кода.

Но если кажется, что для настройки обработчика сообщений требуется слишком много работы, когда (кажется) это можно было бы обработать с помощью приятного интерфейса? Я совершенно неправильно понимаю решение?

@PureKrome - спасибо, что подняли это на обсуждение. Не могли бы вы уточнить, что вы подразумеваете под «требуется так много работы для настройки обработчика сообщений»?

Один из способов модульного тестирования HttpClient без подключения к сети:

  1. Создайте новый класс Handler (например, FooHandler), производный от HttpMessageHandler.
  2. Реализуйте метод SendAsync в соответствии с вашими требованиями - не попадайте в сеть/регистрируйте запрос-ответ/и т. д.
  3. Передайте экземпляр этого FooHandler в конструктор HttpClient:
    обработчик var = новый FooHandler();
    клиент var = новый HttpClient (обработчик);

Затем ваш объект HttpClient будет использовать ваш обработчик вместо встроенного HttpClientHandler.

Спасибо,
Сид

Привет @SidharthNabar - Спасибо, что прочитали мою проблему, я тоже очень ценю это.

Создайте новый класс обработчика

Вы только что ответили на мой вопрос :)

Это тоже большой танец, чтобы покачиваться, просто чтобы попросить мой настоящий код не _попадать в сеть_.

Я даже сделал репозиторий HttpClient.Helpers и пакет nuget... просто чтобы упростить тестирование! Сценарий похож на счастливый путь или исключение, созданное конечной точкой сети...

Вот в чем проблема -> можем ли мы не делать все это и ... вместо этого просто издеваться над методом?

Я попытаюсь объяснить с помощью кода ..

Цель: Скачать что-нибудь из интернета.

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

Давайте рассмотрим два примера:

Учитывая интерфейс (если он существует):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

Мой код теперь может выглядеть так...

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

и тестовый класс

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

Ура! Готово.

Хорошо, теперь с текущим способом...

  1. создать новый класс Handler - да, класс!
  2. Внедрить обработчик в сервис
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

Но боль в том, что теперь я должен сделать класс. Ты сделал мне больно.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

И этот класс начинается довольно просто. До тех пор, пока у меня не будет несколько GetAsync calls (поэтому мне нужно предоставить несколько экземпляров Handler?) или несколько httpClients в одной службе. Кроме того, мы Dispose обработчика или повторно используем его, если мы делаем несколько вызовов в одном и том же блоке логики (что является опцией ctor)?

например.

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

с этим можно сделать намного проще..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

чисто чисто чисто :)

Затем - есть следующий бит: возможность обнаружения

Когда я впервые начал использовать MS.Net.Http.HttpClient , API было довольно очевидным :+1: Хорошо - получить строку и сделать это асинхронно.. просто...

... но потом я попал на тестирование .... и теперь я должен узнать о HttpClientHandlers ? Эм почему? Я чувствую, что все это должно быть скрыто под обложками, и мне не нужно беспокоиться обо всех этих деталях реализации. Это слишком много! Вы говорите, что я должен начать заглядывать внутрь коробки и изучать кое-что из сантехники... что больно :cry:

Все это делает это более сложным, чем должно быть, ИМО.


Помоги мне Microsoft - ты моя единственная надежда.

Я тоже хотел бы увидеть простой и простой способ протестировать различные вещи, использующие HttpClient. :+1:

Спасибо за подробный ответ и фрагменты кода - это действительно помогает.

Во-первых, я заметил, что вы создаете новые экземпляры HttpClient для отправки каждого запроса — это не предполагаемый шаблон проектирования для HttpClient. Создание одного экземпляра HttpClient и его повторное использование для всех ваших запросов помогает оптимизировать пул соединений и управление памятью. Рассмотрите возможность повторного использования одного экземпляра HttpClient. Как только вы это сделаете, вы можете вставить поддельный обработчик только в один экземпляр HttpClient, и все готово.

Второе - вы правы. Дизайн API HttpClient не подходит для чисто интерфейсного механизма тестирования. Мы рассматриваем возможность добавления в будущем шаблона статического метода/фабрики, который позволит вам изменить поведение всех экземпляров HttpClient, созданных из этой фабрики или после этого метода. Однако мы еще не определились с дизайном для этого. Но ключевой вопрос останется — вам нужно будет определить поддельный обработчик и вставить его ниже объекта HttpClient.

@ericsstj - мысли по этому поводу?

Спасибо,
Сид.

@SidharthNabar , почему вы / команда так не решаетесь предложить интерфейс для этого класса? Я надеялся, что весь смысл того, что разработчик должен _узнавать_ об обработчиках, а затем _создавать_ поддельные классы обработчиков, достаточен для того, чтобы оправдать (или, по крайней мере, подчеркнуть) необходимость интерфейса?

Да, я не понимаю, почему интерфейс может быть плохим. Это значительно упростило бы тестирование HttpClient.

Одна из основных причин, по которой мы избегаем интерфейсов в фреймворке, заключается в том, что они плохо версионируются. Люди всегда могут создать свой собственный, если у них есть потребность, и он не сломается, когда в следующей версии фреймворка потребуется добавить нового члена. @KrzysztofCwalina или @terrajobst могли бы рассказать больше об истории/деталях этого руководства по дизайну. Этот конкретный вопрос является почти религиозным спором: я слишком прагматичен, чтобы принимать в этом участие.

HttpClient имеет множество вариантов модульного тестирования. Он не запечатан, и большинство его членов виртуальны. У него есть базовый класс, который можно использовать для упрощения абстракции, а также тщательно разработанная точка расширения в HttpMessageHandler, как указывает @sharwell . ИМО, это довольно хорошо разработанный API, благодаря @HenrikFrystykNielsen.

:+1: интерфейс

:wave: @ericsstj Огромное спасибо, что заглянули :+1: :cake:

Одна из основных причин, по которой мы избегаем интерфейсов в фреймворке, заключается в том, что они плохо версионируются.

Ага - отличный момент.

Этот конкретный вопрос является почти религиозным спором

да ... точка хорошо взята на себя.

HttpClient имеет множество опций для модульного тестирования.

Ой? Я борюсь с этим :blush: отсюда и причина этой проблемы :blush:

Он не запечатан, и большинство его членов виртуальны.

Участники виртуальные? О черт, я совсем пропустил это! Если они виртуальные, то мокирующие фреймворки могут издеваться над этими членами :+1: и нам не нужен интерфейс!

Давайте взглянем на HttpClient.cs ...

public Task<HttpResponseMessage> GetAsync(string requestUri) { .. }
public Task<HttpResponseMessage> GetAsync(Uri requestUri) { .. }

Хм. они не виртуальные ... давайте найдем файл ... э-э ... ключевое слово virtual не найдено. Значит, вы имеете в виду, что _другие_ члены _других связанных_ классов виртуальны? Если да... то мы возвращаемся к проблеме, которую я поднимаю - теперь мы должны заглянуть под капот, чтобы увидеть, что делает GetAsync , чтобы мы знали, что создавать/подключать/и т.д...

Думаю, я просто не понимаю чего-то действительно основного, здесь ¯(°_°)/¯ ?

РЕДАКТИРОВАТЬ: может быть, эти методы могут быть виртуальными? Я умею пиариться!

SendAsync является виртуальным, как и почти все остальные уровни API. «Большинство» было неверным, тут моя память мне не изменяет. У меня сложилось впечатление, что большинство из них были фактически виртуальными, поскольку они строятся поверх виртуального члена. Обычно мы не делаем вещи виртуальными, если они вызывают каскадную перегрузку. Существует более конкретная перегрузка SendAsync, которая не является виртуальной, и ее можно исправить.

Ах! Попался :blush: Так что все эти методы заканчиваются вызовом SendAsync .. который делает всю тяжелую работу. Так что это по-прежнему означает, что у нас есть проблема с возможностью обнаружения... но давайте отложим это в сторону (это самоуверенно).

Как бы мы издевались над SendAsync , давая этот базовый образец...
Install-Package Microsoft.Net.Http
Install-Package xUnit

public class SomeService
{
    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }
}

public class Facts
{
    [Fact]
    public async Task GivenAValidUrl_GetSomeData_ReturnsSomeData()
    {
        // Arrange.
        var service = new SomeService();

        // Act.
        var result = await service.GetSomeData("http://www.google.com");

        // Assert.
        Assert.True(result.Length > 0);
    }
}

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

Я могу полностью понять этот образ мышления с полной .NET Framework.

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

Я не выступаю за то, чтобы вы ломали каждый интерфейс каждый день: в идеале критические изменения должны быть объединены в новую основную версию.

Мне грустно калечить API из соображений совместимости в будущем. Разве одной из целей .NET Core не было ускорение итерации, поскольку вам больше не нужно беспокоиться о малозаметном обновлении .NET, нарушающем работу тысяч уже установленных приложений?

Мои два цента.

@MrJul :+1:
Версии не должны быть проблемой. Вот почему существуют номера версий :)

Версионирование по-прежнему является большой проблемой. Добавление члена в интерфейс является критическим изменением. Для таких основных библиотек, как эта, которые находятся в папке «Входящие» на рабочем столе, мы хотим вернуть функции на рабочий стол в будущих версиях. Если мы разветвимся, это означает, что люди не могут писать переносимый код, который будет работать в обоих местах. Дополнительные сведения о критических изменениях см. по адресу: https://github.com/dotnet/corefx/wiki/Breaking-Changes.

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

Я не очень хорошо знаком с тестовой библиотекой, которую вы используете, но я бы предоставил хук, позволяющий тестам устанавливать экземпляр клиента, а затем создавать фиктивный объект, который ведет себя так, как я хочу. Мок-объект может быть изменчивым, чтобы разрешить повторное использование.

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

Если API нельзя смоделировать, мы должны это исправить. Но это не имеет ничего общего с интерфейсами и классами. Интерфейс ничем не отличается от чистого абстрактного класса с точки зрения макетирования для всех практических целей.

@KrzysztofCwalina вы, сэр, попали в самую точку! идеальное резюме!

Думаю, я выберу менее популярную сторону этого аргумента, поскольку лично я не считаю, что интерфейс необходим. Как уже было указано, HttpClient уже является абстракцией. Поведение действительно происходит от HttpMessageHandler . Да, это невозможно смоделировать/подделать, используя подходы, к которым мы привыкли с такими фреймворками, как Moq или FakeItEasy, но это и не обязательно; это не _единственный_ способ делать вещи. Тем не менее, API по-прежнему отлично тестируется по дизайну.

Итак, давайте рассмотрим вопрос «Мне нужно создать свою собственную HttpMessageHandler ?». Нет, конечно нет. Мы не все написали свои собственные фиктивные библиотеки. @PureKrome уже показал свою библиотеку HttpClient.Helpers . Лично я не использовал это, но я проверю это. Я использую библиотеку @richardszalay MockHttp , которую считаю фантастической. Если вы когда-либо работали с $httpBackend AngularJS, MockHttp использует точно такой же подход.

Что касается зависимостей, ваш класс обслуживания должен позволять вводить HttpClient и, очевидно, может предоставлять разумное значение по умолчанию new HttpClient() . Если вам нужна возможность создавать экземпляры, возьмите Func<HttpClient> или, если вы по какой-либо причине не являетесь поклонником Func<> , создайте собственную абстракцию IHttpClientFactory и реализация по умолчанию, опять же, просто вернет new HttpClient() .

В заключение я считаю, что этот API прекрасно тестируется в его нынешнем виде, и не вижу необходимости в интерфейсе. Да, это требует другого подхода, но вышеупомянутые библиотеки уже существуют, чтобы помочь нам с этим другим стилем тестирования совершенно логичным и интуитивным образом. Если мы хотим большей функциональности/простоты, давайте внесем свой вклад в эти библиотеки, чтобы сделать их лучше.

Лично для меня интерфейс или виртуальные методы не имеют большого значения. Но да ладно, perfectly testable — это слишком много :)

@luisrudge Хорошо, можете ли вы привести сценарий, который нельзя протестировать с использованием стиля тестирования обработчика сообщений, который позволяет что-то вроде MockHttp? Может быть, это помогло бы раскрыть дело.

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

На данный момент я придерживаюсь мнения, что это «идеально тестируемая» зависимость, просто не так, как привыкли разработчики .NET.

Это можно проверить, согласен. но это все еще боль. Если вам нужно использовать внешнюю библиотеку, которая поможет вам в этом, ее нельзя полностью протестировать. Но я думаю, что это слишком субъективно, чтобы обсуждать это.

Можно утверждать, что aspnet mvc 5 отлично тестируется, вам просто нужно написать 50LOC, чтобы имитировать все, что нужно контроллеру. ИМХО, то же самое и с HttpClient. Это можно проверить? да. Легко проверить? Нет.

@luisrudge Да, я согласен, это субъективно, и я полностью понимаю стремление к интерфейсу/виртуалам. Я просто пытаюсь убедиться, что любой, кто придет и прочитает эту ветку, по крайней мере, получит некоторое представление о том, что этот API _может_ использоваться в кодовой базе очень тестируемым способом, не вводя все ваши собственные абстракции вокруг HttpClient , чтобы добраться туда.

если вам нужно использовать внешнюю библиотеку, которая поможет вам в этом, ее нельзя полностью проверить

Ну, мы все уже используем ту или иную библиотеку для имитации/подделки * и, это правда, мы не можем просто использовать _эту_ для тестирования этого стиля API, но я не думаю, что это означает, что она менее проверяемая, чем подход, основанный на интерфейсе, только потому, что мне нужно использовать другую библиотеку.

_ * По крайней мере, я надеюсь, что нет!_

@ drub0y из моего OP Я заявил, что библиотеку можно протестировать, но это просто настоящая боль по сравнению (с тем, во что я страстно верю) с тем, чем она _могла_ быть. ИМО @luisrudge отлично объяснил это:

Можно утверждать, что aspnet mvc 5 отлично тестируется, вам просто нужно написать 50LOC, чтобы сымитировать все, что нужно контроллеру.

Этот репозиторий является основной частью _большого_ числа разработчиков. Таким образом, тактика по умолчанию (и понятная) заключается в том, чтобы быть очень осторожным с этим репозиторием. Конечно - я полностью понимаю это.

Я хотел бы верить, что этот репозиторий все еще может быть изменен (в отношении API) до того, как он станет RTW. Будущие изменения вдруг становятся _действительно_ трудновыполнимыми, включая _восприятие_ изменения.

Так что с текущим апи - можно ли его протестировать? Ага. Проходит ли он тест Dark Matter Developer? Я лично так не думаю.

Лакмусовая бумажка IMO такова: может ли обычный джо подобрать одну из распространенных/популярных тестовых фреймворков + макетные фреймворки и смоделировать любой из основных методов в этом API? Прямо сейчас - нет. Итак, разработчик должен прекратить то, что он делает, и начать изучать _имплиментацию_ этой библиотеки.

Как уже было указано, HttpClient уже является абстракцией. Поведение действительно происходит от HttpMessageHandler.

Это моя точка зрения - почему вы заставляете нас тратить циклы на выяснение этого, когда цель библиотеки - абстрагировать всю эту магию ... только для того, чтобы сказать: «Хорошо ... так что мы сделали немного магии, но теперь, когда вы хотите издеваться наша магия... извините, вам сейчас придется закатать рукава, поднять крышу библиотеки и начать копаться внутри и т. д.».

Это кажется таким... запутанным.

Мы в состоянии облегчить жизнь стольким людям и постоянно возвращаться к оборонительной позиции: «Это можно сделать, но… прочтите FAQ/Кодекс».

Вот еще один подход к этой проблеме: дайте 2 примера случайным разработчикам, не относящимся к MS ... джо гражданским разработчикам, которые знают, как использовать xUnit и Moq/FakeItEasy/InsertTheHipMockingFrameworkThisYear.

  • Первый API, который использует интерфейс/виртуальный/что угодно... но метод можно высмеивать.
  • Второй API, который является просто общедоступным методом, и для имитации _точки интеграции_ .. им нужно прочитать код.

Это сводится к этому, ИМО.

Посмотрите, какой разработчик может решить эту проблему _и_ остаться довольным.

Если API нельзя смоделировать, мы должны это исправить.

Прямо сейчас это не ИМО, но есть способы успешно обойти это (опять же, это самоуверенно - я соглашусь с этим)

Но это не имеет ничего общего с интерфейсами и классами. Интерфейс ничем не отличается от чистого абстрактного класса с точки зрения макетирования для всех практических целей.

Точно - это деталь реализации. Цель состоит в том, чтобы иметь возможность использовать проверенную фиктивную структуру и вперед.

@luisrudge , если вам нужно использовать внешнюю библиотеку, которая поможет вам в этом, ее нельзя полностью проверить
@ drub0y Ну, мы все уже используем ту или иную библиотеку для насмешек / подделок.

(Надеюсь, я понял последнюю цитату/абзац..) Не... тихо. Вот что @luisrudge сказал: «У нас есть один инструмент для тестирования. Второй инструмент для насмешек. Пока что это общие/общие инструменты, ни к чему не привязанные. Но… теперь вы хотите, чтобы я загрузил третий инструмент, который _специфический_ издеваться над _специфическим_ API/сервисом в нашем коде, потому что этот конкретный API/сервис, который мы используем, не предназначен для хорошего тестирования?». Это немного богато :(

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

Полностью согласен! Итак, можете ли вы реорганизовать приведенный выше пример кода, чтобы показать нам, насколько простым это должно/может быть? Есть много способов содрать шкуру с кошки... так какой простой и легкий способ? Покажите нам код.

Я начну. Извиняюсь перед @KrzysztofCwalina за использование интерфейса, но это только для того, чтобы запустить мою лакмусовую бумажку.

public interface IHttpClient
{
    Task<string> GetStringAsync(string url);
}

public class SomeService
{
    private IHttpClient _httpClient;

    // Injected.
    public SomeService(IHttpClient httpClient) { .. }

    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }    
}

Похоже, что все, что нужно @PureKrome , — это обновление документации, объясняющее, какие методы имитировать/переопределять, чтобы настроить поведение API во время тестирования.

@sharwell это абсолютно не так. Когда я тестирую материал, я не хочу запускать весь код httpclient:
Посмотрите метод SendAsync .
Это безумие. Вы предлагаете, чтобы каждый разработчик знал внутренности класса, открывал github, видел код и тому подобное, чтобы иметь возможность протестировать его. Я хочу, чтобы этот разработчик издевался над GetStringAsync и делал дерьмо.

@luisrudge На самом деле я бы посоветовал вообще избегать насмешек над этой библиотекой. Поскольку у вас уже есть чистый уровень абстракции с несвязанной, заменяемой реализацией, дополнительные насмешки не нужны. Насмешка для этой библиотеки имеет следующие дополнительные недостатки:

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

Мокирование — это стратегия тестирования, нацеленная на переплетенные кодовые базы, которые трудно протестировать в небольшом масштабе. В то время как использование имитации коррелирует с увеличением затрат на разработку, _необходимость_ для имитации коррелирует с увеличением количества связей в коде. Это означает, что насмешка сама по себе является запахом кода. Если вы можете обеспечить одинаковое покрытие входных и выходных тестов в вашем API без использования имитации, вы выиграете практически во всех аспектах разработки.

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

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

Я согласен с комментарием @luisrudge ранее. _Технически_ MVC5 можно было протестировать, но, боже мой, это была заноза в заднице и огромный источник дергания за волосы.

@sharwell Вы говорите, что насмешка над IHttpClient — это бремя , а реализация фальшивого обработчика сообщений (как этот — нет? Я не могу с этим согласиться, извините.

@luisrudge Для простых случаев тестирования GetStringAsync не так сложно издеваться над обработчиком. Используя готовый Moq + HttpClient, все, что вам нужно, это:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) }));
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

Если это слишком много, вы можете определить вспомогательный класс:

internal static class MockHttpClientHandlerExtensions
{
    public static void SetupGetStringAsync(this Mock<HttpClientHandler> mockHandler, Uri requestUri, string response)
    {
        mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
            .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response) }));
    }
}

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

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.SetupGetStringAsync(requestUri, expectedResponse);
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

Итак, для сравнения двух версий:
_Отказ от ответственности_: это непроверенный код браузера. Кроме того, я давно не использовал Moq.

Текущий

public class SomeService(HttpClientHandler httpClientHandler= null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null
                                  ? new HttpClient
                                  : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit test...
// Arrange.
Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);
var someService = new SomeService(mockHandler.Object); // Injected...

// Act.
string result = await someService.GetSomethingFromTheInternetAsync();

// Assert.
Assert.AreEqual(expectedResponse, result);
httpClient.Verify(x => x.GetSomethingFromTheInternetAsync(), Times.Once);

вместо того, чтобы предлагать способ напрямую издеваться над методом API.

Другой путь

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit tests...
// Arrange.
var response = new Foo();
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);
var service = new SomeService(httpClient.Object); // Injected.

// Act.
var result = await service.GetSomethingFromTheInternetAsync();

// Assert.
Assert.Equals("something", foo.Something);
httpClient.Verify(x => x.GetStringAsync(It.IsAny<string>()), Times.Once);

перегоняя это, в основном это сводится к этому (за исключением небольшой разницы в SomeService ) ...

var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);

против этого...

var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);

Подходит ли код _грубо_ для обоих? (оба должны быть достаточно точными, прежде чем мы сможем их сравнить).

@sharwell , поэтому мне нужно запустить свой код через весь код HttpClient.cs, чтобы запустить мои тесты? Как это меньше связано с HttpClient?

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

Престижность @PureKrome за его библиотеку, но было бы лучше, если бы в ней не было необходимости :)

Я только что прочитал все комментарии и многому научился, но у меня есть один вопрос: зачем вам нужно издеваться над HttpClient в ваших тестах?

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

Другой возможный сценарий, который потребует такой имитации, — это когда вам нужно проанализировать загруженную строку, и этот фрагмент кода требует тестирования для нескольких разных выходных данных. Даже в этом случае эту функциональность (парсинг) можно было бы извлечь в класс/метод и протестировать без каких-либо моков.

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

@tucaz Давайте представим, что у меня есть веб-сайт, который возвращает некоторую информацию о вас. Ваш возраст, имя, день рождения и т. д. Вы также владеете акциями некоторых компаний.

Вы сказали, что владеете 100 акциями AAA и 200 акциями BBB? Таким образом, когда мы отображаем данные вашего счета, мы _должны_ отображать, сколько денег стоят ваши акции.

Для этого нам нужно получить текущие цены акций из _другой_ системы. Итак, чтобы сделать это, нам нужно получить акции с помощью службы, верно? Давай к этому...

_Отказ от ответственности: код браузера..._

public class StockService : IStockService
{
    private readonly IHttpClient _httpClient;

    public StockService(IHttpClient httpClient)
    {
        _httpClient = httpClient; // Optional.
    }

    public async Task<IEnumerable<StockResults>> GetStocksAsync(IEnumerable<string> stockCodes)
    {
        var queryString = ConvertStockCodeListIntoQuerystring();
        string jsonResult;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            jsonResult = await httpClient.GetStringAsync("http:\\www.stocksOnline.com\stockResults?" + queryString);
        }

        return JsonConvert.DeserializeObject<StockResult>(jsonResult);
    }
}

Итак, у нас есть простой сервис, который говорит: «_Иди, дай мне цены на акции AAA и BBB_»;

Итак, мне нужно проверить это:

  • Учитывая некоторые законные названия акций
  • Учитывая некоторые недопустимые/несуществующие названия акций
  • httpClient выдает исключение (интернет взорвался в ожидании ответа)

и теперь я могу легко проверить эти сценарии. Я замечаю, что когда httpClient взрывается (выдает исключение) ... эта служба взрывается ... так что, может быть, мне нужно добавить некоторые журналы и вернуть ноль? Donno .. но, по крайней мере, я могу подумать о проблеме, а затем решить ее.

Конечно, я на 100% не хочу по-настоящему поражать реальный веб-сервис во время моих обычных модульных тестов. Внедрив макет, я могу это использовать. В противном случае я могу создать новый экземпляр и использовать его.

Итак, я пытаюсь установить, что мое предложение намного проще (читать/поддерживать/и т. д.), чем текущий статус-кво.

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

Зачем нам обертывать HttpClient? Это _уже_ абстракция над всем http :sparkles: внизу. Это отличная библиотека! Я лично не понимаю, чего бы это добилось?

@PureKrome ваш пример использования — это именно то, что я имею в виду, говоря о заключении функциональности в класс-оболочку.

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

Пожалуйста, обратите внимание на следующее:

private IStockService stockService;

[Setup]
public void Setup()
{
    stockService = A.Fake<IStockService>();

    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker1" }))
        .Returns(new StockResults[] { new StockResults { Price = 100m, Ticker = "Ticker1", Status = "OK" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker2" }))
        .Returns(new StockResults[] { new StockResults { Price = 0m, Ticker = "Ticker2", Status = "NotFound" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker3" }))
        .Throws(() => new InvalidOperationException("Some weird message"));
}

[Test]
public async void Get_Total_Stock_Quotes()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker1" });

    Assert.IsNotNull(total);
    Assert.IsGreaterThan(0, total.Sum(x => x.Price);
}

[Test]
public async void Hints_When_Ticker_Not_Found()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker2" });

    Assert.IsNotNull(total);
    Assert.AnyIs(x => x.Status == "NotFound");
}

[Test]
public async void Throws_InvalidOperationException_When_Error()
{
    var stockService = A.Fake<IStockService>();

    Assert.Throws(() => await stockService.GetStockAsync(new [] { "Ticker3" }), typeof(InvalidOperationException));
}

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

Единственный сценарий, который я могу придумать, чтобы сымитировать HttpClient , — это когда вам нужно проверить некоторую логику повтора в зависимости от ответа, полученного от сервера, или что-то в этом роде. В этом случае вы будете создавать более сложный фрагмент кода, который, вероятно, будет извлечен в класс HttpClientWithRetryLogic и не будет распространяться в каждом кодируемом вами методе, использующем класс HttpClient .

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

@tucaz Разве вы только что не проверили, что ваша подделка возвращает то, что вы настроили, эффективно тестируя библиотеку для насмешек? @PureKrome хочет протестировать конкретную реализацию StockService .

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

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

@MrJul , ты прав. Вот почему я сказал, что то, что было протестировано на его примере, — это сама реализация веб-сервиса, и почему я предложил (без примеров кода. Плохо), что он издевается над IStockService , чтобы он мог утверждать, что код, потребляющий его, способен обрабатывать все, что возвращает GetStockAsync .

Это то, что я думаю, должно быть проверено в первую очередь. Не издевательский фреймворк (как я) или реализация веб-сервиса (как хочет сделать @PureKrome ).

В конце концов, я могу понять, что было бы неплохо иметь интерфейс IHttpClient , но не вижу никакой полезности в таких сценариях, поскольку IMO необходимо протестировать то, как потребители абстракции службы (и, следовательно, веб-сервис) может правильно обработать его возврат.

@tucaz нет. Ваш тест неверен. Вы тестируете мокационную библиотеку. Используя пример @PureKrome , можно было бы проверить три вещи:

  • если HTTP-запрос использует правильный METHOD
  • если http запрос имеет право URL
  • если тело ответа может быть успешно десериализовано с помощью десериализатора json

Если вы просто имитируете IStockService в своем контроллере (например), вы не тестируете ни одну из трех вещей, упомянутых выше.

@luisrudge согласен со всеми вашими пунктами. Но... стоит ли тестировать все эти вещи, которые вы перечисляете? Я имею в виду, что если я посвятил себя тестированию всех тех базовых вещей, которые охватываются несколькими фреймворками (json.net, httpclient и т. д.), я думаю, что смогу справиться так, как это возможно сделать сегодня.

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

Я считаю, что такие тесты, которые вы предлагаете, имеют минимальную чистую ценность, и вы должны работать над ними, только если у вас есть остальные 99% вашего приложения. Но, в конце концов, все зависит от личных предпочтений, и это субъективная тема, в которой разные люди видят разную ценность. И это, конечно, не означает, что HttpClient нельзя тестировать.

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

Цель, которую я имел в виду, когда я впервые выступил, состояла в том, чтобы помочь @PureKrome попытаться не сосредотачивать свои усилия на том, что я считаю малоценной задачей. Но опять же, в какой-то момент все сводится к личному мнению.

Быть уверенным, что вы нажали правильный URL-адрес, имеет низкую чистую ценность для вас? Как насчет того, чтобы убедиться, что вы делаете запрос PUT для редактирования ресурса, а не сообщения? Остальные 99% моего приложения могут быть покрыты, если этот вызов завершится ошибкой, то ничего не будет работать.

Тааак, я действительно думаю, что говорить о HttpClient::GetStringAsync — это плохой способ доказать, что вообще можно издеваться над HttpClient . Если вы используете какой-либо из методов HttpClient::Get[String|Stream|ByteArray]Async , которые вы уже потеряли, потому что вы не можете выполнить правильную обработку ошибок, поскольку все скрыто за HttpRequestException , который будет выброшен, и это не даже не раскрывайте важные детали, такие как код состояния HTTP. Таким образом, это означает, что любая абстракция, которую вы построили поверх HttpClient , не сможет реагировать и преобразовывать определенные ошибки HTTP в исключения, специфичные для домена, и теперь у вас есть дырявая абстракция... и одна это дает вызывающим службам вашего домена ужасную информацию об исключениях.

Я думаю, важно указать на это, потому что в большинстве случаев это означает, что если бы вы использовали насмешливый подход, вы бы издевались над методами HttpClient , которые возвращают HttpResponseMessage , что уже приводит вас к тот же объем работы, который требуется для имитации/подделки HttpMessageHandler::SendAsync . На самом деле, это больше! Возможно, вам придется имитировать несколько методов (например, [Get|Put|Delete|Send]Async ) на уровне HttpClient вместо того, чтобы просто имитировать SendAsync на уровне HttpMessageHandler .

@luisrudge также упомянул десериализацию JSON, так что теперь мы говорим о работе с HttpContent , что означает, что вы работаете с одним из методов, которые точно возвращают HttpResponseMessage . Если вы не говорите, что обходите всю архитектуру средства форматирования мультимедиа и десериализуете строки из GetStringAsync в экземпляры объектов вручную с помощью JsonSerializer::Deserialize или чего-то еще. Если это так, то я не думаю, что мы можем продолжать разговор о _тестировании_ API, потому что это будет просто использование API _самого_ совершенно неправильного. :расстроенный:

@luisrudge это действительно важно, но вам не нужно трогать HttpClient , чтобы убедиться, что это правильно. Просто убедитесь, что источник, из которого вы читаете URL-адрес, возвращает все правильно, и все готово. Опять же, не нужно делать интеграционный тест с HttpClient в нем.

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

@tucaz согласен. Тем не менее: это самая важная часть, и я хотел бы ее протестировать :)

@ drub0y Я поражен, что использование общедоступного метода неправильно :)

@luisrudge Ну, я полагаю, это другое обсуждение, но я не чувствую, что оно приводит доводы в пользу интерфейса, который можно высмеивать, игнорируя присущие недостатки использования этих «удобных» методов во всех сценариях кодирования, кроме самых корректирующих. (например, служебные приложения и сценические демонстрации). Если вы решите использовать их в рабочем коде, это ваш выбор, но вы делаете это, надеюсь, признавая их недостатки.

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

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

``` С#
открытый интерфейс IsomeDomainService
{
ЗадачаGetSomeThingsValueAsync (идентификатор строки);
}

открытый класс SomeDomainService : ISomeDomainService
{
частный HttpClient только для чтения _httpClient;

public SomeDomainService() : this(new HttpClient())
{

}

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

public async Task<string> GetSomeThingsValueAsync(string id)
{
    return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
}

}

As you can see, it allows an `HttpClient` instance to be injected and also has a default constructor that instantiates the default `HttpClient` with no other special settings (obviously this default instance can be customized, but I'll leave that out for brevity).

Ok, so what's a test method look like for this thing using the MockHttp library then? Let's see:

``` c#
[Fact]
public async Task GettingSomeThingsValueReturnsExpectedValue()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();
    mockHttpMessageHandler.Expect("http://unittest/things/123")
        .Respond(new StringContent("expected value"));

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    var value = await sut.GetSomeThingsValueAsync("123");

    // Assert
    value.Should().Be("expected value");
}

Эээ, это довольно просто и прямолинейно; читается ясно как божий день ИМНШО. Кроме того, обратите внимание, что я использую FluentAssertions, которая, опять же, является еще одной библиотекой, которую, я думаю, используют многие из нас, и еще одним ударом по аргументу «Я не хочу приносить другую библиотеку».

Теперь позвольте мне также проиллюстрировать, почему вы, вероятно, никогда не захотите использовать GetStringAsync для любого производственного кода. Оглянитесь назад на метод SomeDomainService::GetSomeThingsValueAsync и предположите, что он вызывает REST API, который на самом деле возвращает осмысленные коды состояния HTTP. Например, возможно, «вещь» с идентификатором «123» не существует, поэтому API вернет статус 404. Итак, я хочу обнаружить это и смоделировать его как следующее исключение для домена:

``` С#
// поддержка сериализации исключена для краткости
открытый класс SomeThingDoesntExistException: исключение
{
public SomeThingDoesntExistException (идентификатор строки)
{
идентификатор = идентификатор;
}

public string Id
{
    get;
    private set;
}

}

Ok, so let's rewrite the `SomeDomainService::GetSomeThingsValueAsync` method to do that now:

``` c#
public async Task<string> GetSomeThingsValueAsync(string id)
{
    try
    {
        return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
    }
    catch(HttpRequestException requestException)
    {
        // uh, oh... no way to tell if it doesn't exist (404) or server maybe just errored (500)
    }
}

Именно об этом я и говорил, когда говорил, что базовые «удобные» методы вроде GetStringAsync не совсем «хороши» для кода производственного уровня. Все, что вы можете получить, это HttpRequestException , и оттуда вы не можете сказать, какой код состояния вы получили, и, следовательно, не можете преобразовать его в правильное исключение в домене. Вместо этого вам нужно будет использовать GetAsync и интерпретировать HttpResponseMessage следующим образом:

``` С#
общедоступная асинхронная задачаGetSomeThingsValueAsync (идентификатор строки)
{
HttpResponseMessage responseMessage = await _httpClient.GetAsync("/things/" + Uri.EscapeUriString(id));

if(responseMessage.IsSuccessStatusCode)
{
    return await responseMessage.Content.ReadAsStringAsync();
}
else
{
    switch(responseMessage.StatusCode)
    {
        case HttpStatusCode.NotFound:
            throw new SomeThingDoesntExistException(id);

        // any other cases you want to might want to handle specifically for your domain

        default:
            // Unhandled cases can throw domain specific lower level communication exceptions
            throw new HttpCommunicationException(responseMessage.StatusCode, responseMessage.ReasonPhrase);
    }
}

}

Ok, so now let's write a test to validate that I get my domain specific exception when I request a URL that doesn't exist:

``` C#
[Fact]
public void GettingSomeThingsValueForIdThatDoesntExistThrowsExpectedException()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    Func<Task> action = async () => await sut.GetSomeThingsValueAsync("SomeIdThatDoesntExist");

    // Assert
    action.ShouldThrow<SomeThingDoesntExistException>();
}

Ermahgerd кода еще меньше!!$!% Почему? Потому что мне даже не нужно было настраивать ожидание с помощью MockHttp, и он автоматически вернул мне 404 для запрошенного URL-адреса в качестве поведения по умолчанию.

Magic is Real

@PureKrome также упомянул случай, когда он хочет создать новые экземпляры HttpClient из класса обслуживания. Как я уже говорил ранее, в этом случае вы бы взяли Func<HttpClient> в качестве зависимости вместо того, чтобы абстрагироваться от фактического создания, или вы могли бы ввести свой собственный фабричный интерфейс, если вы не поклонник Func по любой причине.

Я удалил свой ответ - я хочу подумать над ответом @drub0y и немного написать код.

@ drub0y , вы забыли упомянуть, что кто-то должен будет понять, что в первую очередь нужно реализовать MockHttpMessageHandler . В этом весь смысл обсуждения. Это недостаточно просто. С aspnet5 вы можете тестировать все, просто имитируя публичный класс из интерфейсов или базовые классы с виртуальными методами, но эта конкретная запись API отличается, менее обнаруживаема и менее интуитивно понятна.

@luisrudge Конечно, я могу с этим согласиться, но как мы вообще поняли, что нам нужен Moq или FakeItEasy? Как мы поняли, что нам нужны N/XUnit, FluentAssertions и т. д.? Я действительно не вижу в этом никакой разницы.

В сообществе есть замечательные (умные) люди, которые осознают необходимость этого материала и действительно тратят время на запуск таких библиотек для решения этих проблем. (Спасибо им всем!) Затем решения, которые действительно «хорошие», замечают другие, которые осознают те же потребности и начинают органично расти через OSS и евангелизацию со стороны других лидеров сообщества. Я думаю, что в этом отношении дела обстоят лучше, чем когда-либо, поскольку возможности OSS наконец-то осознаются в сообществе .NET.

При этом я лично считаю, что Microsoft должна была предоставить что-то вроде MockHttp вместе с API System.Net.Http прямо «из коробки». Команда AngularJS предоставила $httpBackEnd вместе с $http , потому что они поняли, что людям абсолютно _нужна_ эта возможность, чтобы иметь возможность эффективно тестировать свои приложения. Я думаю, что тот факт, что Microsoft _не_ идентифицировала эту же проблему, является причиной того, что существует так много «путаницы» вокруг лучших практик с этим API, и я думаю, почему мы все здесь обсуждаем это сейчас. :disappointed: Тем не менее, я рад, что @richardszalay и @PureKrome взялись за дело, чтобы попытаться заполнить этот пробел своими библиотеками, и теперь я думаю, что мы должны поддержать их, чтобы помочь улучшить их библиотеки, однако мы можем сделать все возможное. нашей жизни легче. :улыбка:

Только что получил уведомление об этой теме, поэтому решил добавить свои 2c.

Я использую динамические мок-фреймворки так же часто, как и любой другой парень, но в последние годы я пришел к выводу, что некоторые домены (например, HTTP) лучше обслуживаются, когда их подделывают под чем-то с большим знанием предметной области. Все, что выходит за рамки базового сопоставления URL-адресов, было бы довольно болезненным при использовании динамического издевательства, и по мере повторения этого вы в конечном итоге получите свою собственную мини-версию того, что MockHttp все равно делает.

Возьмем Rx в качестве другого примера: IObservable — это интерфейс, и, следовательно, его можно имитировать, но было бы безумием тестировать сложные композиции (не говоря уже о планировщиках) только с динамическим имитированием. Библиотека тестирования Rx придает гораздо большее семантическое значение предметной области, в которой она работает.

Возвращаясь к HTTP, в частности, хотя в мире JS вы также можете использовать sinon для имитации XMLHttpRequest, а не использовать API-интерфейсы тестирования Angular, но я был бы очень удивлен, если бы кто-то это сделал.

Сказав все это, я полностью согласен с открываемостью. Документация по HTTP-тестированию Angular находится рядом с его документацией по HTTP, но вам нужно знать о MockHttp, чтобы найти ее. Таким образом, я был бы полностью согласен с возвращением MockHttp в основную библиотеку, если кто-либо из команды CoreFx захочет связаться с ним (лицензии уже те же).

Мне нравится издеваться над этим, но мы также используем подпись Func :

public static async Task<T> GetWebObjectAsync<T>(Func<string, Task<string>> getHtmlAsync, string url)
{
    var html = await getHtmlAsync(url);
    var info = JsonConvert.DeserializeObject<T>(html);
    return info;
}

Это позволяет сделать код тестового класса таким простым:

Func<string, Task<string>> getHtmlAsync = u => Task.FromResult(EXPECTED_HTML);
var result = controller.InternalCallTheWeb(getHtmlAsync);

И контроллер может просто сделать:

public static HttpClient InitHttpClient()
{
    return _httpClient ?? (_httpClient = new HttpClient(new WebRequestHandler
    {
        CachePolicy = new HttpRequestCachePolicy(HttpCacheAgeControl.MaxAge, new TimeSpan(0, 1, 0)),
        Credentials = new NetworkCredential(PublishingCredentialsUserName, PublishingCredentialsPassword)
    }, true));
}

[Route("Information")]
public async Task<IHttpActionResult> GetInformation()
{
    var httpClient = InitHttpClient();
    var result = await InternalCallTheWeb(async u => await httpClient.GetStringAsync(u));
    ...
}

Но они могли бы сделать это немного проще... :)

Привет, команда .NET! Просто касаясь базы по этому вопросу. Он существует уже больше месяца, и мне просто любопытно узнать, что команда думает об этом обсуждении.

Были разные моменты с обеих сторон лагеря - все интересные и актуальные, ИМО.

Есть какие-нибудь обновления от вас, девочки/ребята?

т.е.

  1. Мы читаем разговор и ничего не изменим. Спасибо за ваш вклад, хорошего дня. Вот, возьми кусочек :cake:
  2. Мы, честно говоря, не думали об этом (здесь все чертовски занято, как вы можете себе представить) _но_ подумаем об этом позже, _до_ того, как мы выпустим Release-To-Web (RTW).
  3. На самом деле, у нас было несколько внутренних дискуссий по этому поводу, и мы признаем, что общественность находит немного болезненным грок. Итак, мы думаем, что могли бы сделать XXXXXXXXX.

та!

Это часть 2 и 3.

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

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

Большое спасибо @davidsh за очень быстрый ответ :+1:

Я с радостью буду ждать и смотреть это пространство, так как интересно знать, что это не часть 1 :smile: .. и с нетерпением жду, когда же произойдут какие-либо изменения :+1:

Еще раз спасибо (команда) за ваше терпение и внимание — очень, очень ценю это!

/me радостно ждет.

@davidsh , так что о виртуальных методах интерфейса ОС не может быть и речи?

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

А виртуальные методы?

Да, добавление виртуальных методов — это не критическое изменение, а аддитивное. Мы все еще работаем над тем, какие конструктивные изменения возможны в этой области.

/me бросает Resurrection на эту тему...

~ нить скручивается, стонет, дергается и оживает!


Только что слушал Community StandUp 20 августа 2015 года, и они упомянули, что HttpClient будет доступен для кроссплатформенной версии с Beta7. УРА!

Итак... было ли какое-нибудь решение по этому поводу? Не предлагая ни / или .. просто вежливо прося каких-либо обновлений обсуждения и т. д.?

@PureKrome Решение по этому поводу еще не принято, но эта работа по разработке / исследованию API включена в наши планы на будущее. Прямо сейчас большинство из нас очень сосредоточены на том, чтобы разместить остальную часть кода System.Net на GitHub и работать кроссплатформенно. Это включает в себя как существующий исходный код, так и перенос наших тестов на платформу Xunit. Как только эти усилия сойдутся, у команды будет больше времени, чтобы сосредоточиться на будущих изменениях, таких как то, что предлагается в этой теме.

Большое спасибо @davidsh - я очень ценю ваше время, чтобы ответить :) Продолжайте в том же духе! :баллон:

Разве использование макетов для тестирования http-сервисов не устарело? Существует довольно много вариантов самостоятельного хостинга, которые позволяют легко издеваться над службой http и не требуют изменений кода, только изменение URL-адреса службы. Т.е. HttpListener и собственный хост OWIN, но для сложного API можно было бы даже создать фиктивный сервис или использовать автоответчик.

image

image

Есть какие-либо обновления о возможности модульного тестирования этого класса HttpClient? Я пытаюсь смоделировать метод DeleteAsync, но, как упоминалось ранее, Moq жалуется на то, что функция не является виртуальной. Кажется, нет более простого способа, чем создать собственную оболочку.

Я голосую за Richardszalay.Mockhttp!
Это превосходно! Просто и гениально!

https://github.com/richardszalay/mockhttp

но @YehudahA - нам это не нужно .... (в этом смысл этого разговора) :)

@PureKrome нам не нужно IHttpClient .
Это та же идея, что и в EF 7. Вы никогда не используете IDbContext или IDbSet, а просто меняете поведение с помощью DbContextOptions.

Richardszalay.MockHttp — готовое решение.

Это нестандартное решение.

нам не нужен IHttpClient.
Это та же идея, что и в EF 7. Вы никогда не используете IDbContext или IDbSet, а просто меняете поведение с помощью DbContextOptions.

Это нормально - я полностью не согласен, поэтому мы соглашаемся не соглашаться тогда.

Оба способа работают? да. Некоторые люди (такие как я) считают, что имитировать интерфейс или виртуальный метод ЛЕГЧЕ по таким причинам, как легкость обнаружения, читабельность и простота.

Другие (как и вы) любят манипулировать поведением. Я нахожу это более сложным, трудным для обнаружения и более трудоемким. (что (для меня) означает, что его сложнее поддерживать/поддерживать).

Каждому свое, наверное :)

Richardszalay говорит: «Этот шаблон во многом основан на $httpBackend в AngularJS».
Вот так. Давайте посмотрим здесь: https://docs.angularjs.org/api/ngMock/service/ $httpBackend

@PureKrome
Да проще.
Вам не нужно знать, какие классы и методы используются, вам не нужна фиктивная библиотека/интерфейсы/виртуальные методы, настройка не изменится, если вы переключитесь с HttpClient на что-то другое.

        private static IDisposable FakeService(string uri, string response)
        {
            var httpListener = new HttpListener();
            httpListener.Prefixes.Add(uri);

            httpListener.Start();

            httpListener.GetContextAsync().ContinueWith(task =>
            {
                var context = task.Result;

                var buffer = Encoding.UTF8.GetBytes(response);

                context.Response.OutputStream.Write(buffer, 0, buffer.Length);

                context.Response.OutputStream.Close();
            });

            return httpListener;
        }

Применение:

            var uri = "http://localhost:8888/myservice/";
            var fakeResponse = "Hello World";

            using (FakeService(uri, fakeResponse))
            {
                // Run your test here ...

                // This is only to test that is really working:
                using (var client = new HttpClient())
                {
                    var result = client.GetStringAsync(uri).Result;
                }
            }

@metzgithub — лучший здесь, поскольку он позволяет правильно моделировать поведение целевого сервиса.

Это именно то, что я искал, так как это дает мне идеальное место для размещения «неверно сформированных/вредоносных» данных (для тестов безопасности и не-счастливого пути)

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

Я выбираю проектирование и разработку компонентов и сервисов, которые заявляют о своей зависимости от других компонентов. Я настоятельно предпочитаю объявить эти зависимости через конструктор и во время выполнения полагаться на контейнер IoC для внедрения конкретной реализации и управления на этом этапе, является ли он одноэлементным или переходным экземпляром.

Я предпочитаю использовать определение тестируемости, которое я выучил около десяти лет назад, применяя относительно новую тогда практику разработки через тестирование:

«Класс можно тестировать, если его можно удалить из обычной среды и он по-прежнему будет работать после внедрения всех его зависимостей».

Другой выбор, который я предпочитаю, — это тестирование взаимодействия моих компонентов с теми, от которых они зависят. Если я объявляю зависимость от компонента, я хочу проверить, что мой компонент использует ее правильно.

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

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

Я тоже немного опоздал на вечеринку, но даже после прочтения всех предыдущих комментариев я все еще не уверен, что делать дальше. Не могли бы вы рассказать мне, как я могу проверить, что определенные методы в моем тестируемом объекте вызывают определенные конечные точки в общедоступном API? Мне все равно, что вернется. Я просто хочу убедиться, что когда кто-то вызывает «GoGetSomething» для экземпляра моего класса ApiClient, он отправляет GET на « https://somenifty.com/api/something ». Что я хотел бы сделать, так это (не рабочий код):

Mock<HttpClient> mockHttpClient = new Mock<HttpClient>();
mockHttpClient.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>())).Verifiable();
ApiClient client = new ApiClient(mockHttpClient.Object);

Something response = await client.GoGetSomething();
mockHttpClient
    .Verify(x => x.SendAsync(
        It.Is<HttpRequestMessage>(m =>
            m.Method.Equals(HttpMethod.Get) &&
            m.RequestUri.Equals("https://somenifty.com/api/something"))));

Но это не позволяет мне издеваться над SendAsync.

ЕСЛИ бы я мог заставить эту базовую вещь работать, тогда я мог бы проверить, как мой класс обрабатывает определенные ВОЗВРАТЫ из этого вызова.

Есть идеи? Это не кажется (мне) чем-то слишком необычным, чтобы его можно было протестировать.

Но это не позволяет мне издеваться над SendAsync.

Вы не издеваетесь над методом HttpClient.SendAsync .

Вместо этого вы создаете фиктивный обработчик, производный от HttpMessageHandler . Затем вы переопределяете метод SendAsync . Затем вы передаете этот объект обработчика в конструктор HttpClient .

@jdcrutchley , вы можете сделать это с помощью MockHttp @richardszalay.

public class MyApiClient
{
    private readonly HttpClient httpClient;

    public MyApiClient(HttpMessageHandler handler)
    {
        httpClient = new HttpClient(handler, disposeHandler: false);
    }

    public async Task<HttpStatusCode> GoGetSomething()
    {
        var response = await httpClient.GetAsync("https://somenifty.com/api/something");
        return response.StatusCode;
    }
}

[TestMethod]
public async Task MyApiClient_GetSomething_Test()
{
    // Arrange
    var mockHandler = new MockHttpMessageHandler();
    mockHandler.Expect("https://somenifty.com/api/something")
        .Respond(HttpStatusCode.OK);

    var client = new MyApiClient(mockHandler);

    // Act
    var response = await client.GoGetSomething();

    // Assert
    Assert.AreEqual(response, HttpStatusCode.OK);
    mockHandler.VerifyNoOutstandingExpectation();
}

@jdcrutchley или вы создаете свой СОБСТВЕННЫЙ класс-оболочку и интерфейс оболочки с помощью одного метода SendAsync .. который просто вызывает _real_ httpClient.SendAsync (в вашем классе-оболочке).

Я предполагаю, что это то, что делает MockHttp @richardszalay.

Это общий шаблон, который делают люди, когда они не могут что-то издеваться (потому что нет интерфейса ИЛИ он не виртуальный).

mockhttp — это фиктивный DSL на HttpMessageHandler. Это ничем не отличается от динамического издевательства над ним с помощью Moq, но цель более ясна благодаря DSL.

Опыт одинаков в любом случае: смоделируйте HttpMessageHandler и создайте из него конкретный HttpClient. Ваш компонент домена зависит либо от HttpClient, либо от HttpMessageHandler.

Редактировать: TBH, я не совсем уверен, в чем проблема с существующим дизайном. Разница между «фиктивным HttpClient» и «фиктивным HttpMessageHandler» буквально составляет одну строку кода в вашем тесте: new HttpClient(mockHandler) , и у него есть то преимущество, что реализация поддерживает один и тот же дизайн для «Разной реализации для каждой платформы» и « Другая реализация для тестирования» (т. е. не испорчена для целей тестового кода).

Существование таких библиотек, как MockHttp, служит только для предоставления более чистого, специфичного для предметной области тестового API. Создание макета HttpClient через интерфейс не изменит этого.

Как новичок в HttpClient API, я надеюсь, что мой опыт добавит ценности обсуждению.

Я также был удивлен, узнав, что HttpClient не реализует интерфейс для легкого издевательства. Я искал в Интернете, нашел эту тему и прочитал ее от начала до конца, прежде чем продолжить. Ответ от @drub0y убедил меня пойти по пути простого издевательства над HttpMessageHandler.

Мой выбор фреймворка для насмешек — Moq, вероятно, довольно стандартный для разработчиков .NET. Потратив почти час на борьбу с API Moq, пытаясь настроить и проверить защищенный метод SendAsync, я наконец столкнулся с ошибкой в ​​Moq, связанной с проверкой защищенных универсальных методов. Смотрите здесь .

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

Это отличный момент, Кен, я забыл, что HttpMessageHandler.SendAsync был защищен.

Тем не менее, я согласен с тем, что дизайн API ориентирован в первую очередь на расширяемость платформы, и несложно создать подкласс HttpMessageHandler (см . пример mockhttp ). Это может быть даже подкласс, который передает вызов абстрактному общедоступному методу, более удобному для Mock. Я понимаю, что это кажется «нечистым», но вы можете вечно спорить о тестируемости и заражении API-поверхности для целей тестирования.

+1 за интерфейс.

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

Я сейчас руковожу крупным проектом в своей компании, и у меня просто нет времени ходить по таким кроличьим тропам. Интерфейс сэкономил бы мне время, деньги моей компании — и, вероятно, то же самое для многих других.

Я, вероятно, пойду по пути создания собственной оболочки. Потому что, глядя со стороны, никому не будет понятно, почему я внедрил HttpMessageHandler в свой класс.

Если невозможно создать интерфейс для HttpClient, то, возможно, команда .net может создать совершенно новое http-решение, у которого есть интерфейс?

@scott-martin, HttpClient ничего не делает, он просто отправляет запросы на HttpMessageHandler .

Вместо того, чтобы тестировать HttpClient , вы можете тестировать и издеваться над HttpMessageHandler . Это очень просто, и вы также можете использовать richardszalay mockhttp .

@scott-martin, как было упомянуто всего в нескольких комментариях, вы не вводите HttpMessageHandler. Вы вводите HttpClient и вводите _that_ с помощью HttpMessageHandler при модульном тестировании.

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

@YehudahA , я смог заставить его работать, издеваясь над HttpMessageHandler. Но «заставить это работать» - это не то, почему я вмешался. Как сказал @PureKrome , это проблема способности обнаруживать. Если бы был интерфейс, я бы смог смоделировать его и быть в пути за считанные минуты. Поскольку его не было, мне пришлось потратить несколько часов на поиск решения.

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

это проблема способности обнаруживать. Если бы был интерфейс, я бы смог смоделировать его и быть в пути за считанные минуты . Поскольку его не было, мне пришлось потратить несколько часов на поиск решения.

:arrow_up: это. Все дело в этом.

РЕДАКТИРОВАТЬ: акцент, мой.

Хотя я полностью согласен с тем, что это может быть более легко обнаруживаемым, я не уверен, как это может занять «несколько часов». Поиск в Google «mock httpclient» выдает этот вопрос о переполнении стека в качестве первого результата, в котором два ответа с наивысшим рейтингом (хотя, конечно, не принятый ответ) описывают HttpMessageHandler.

НП - мы все же согласны не соглашаться. Это все хорошо :)

вы можете закрыть эту проблему, так как mscorlib возвращается со всем своим багажом :)

Можем ли мы просто реализовать интерфейс в этих классах httpclient? Тогда с остальным можно разобраться самостоятельно.

Какое решение вы предлагаете для имитации HttpClient, используемого в службе DI?
Я пытаюсь реализовать FakeHttpClientHandler и не знаю, что делать с методом SendAsync.

У меня есть простой GeoLocationService, который вызывает микросервис для получения информации о местоположении на основе IP от другого. Я хочу передать поддельный HttpClientHandler службе, у которой есть два конструктора: один получает locationServiceAddress, а другой — locationServiceAddress и дополнительный HttpClientHandler.

public class GeoLocationService: IGeoLocationService {

        private readonly string _geoLocationServiceAddress;
        private HttpClient _httpClient;
        private readonly object _gate = new object();

        /// <summary>
        /// Creates a GeoLocation Service Instance for Dependency Injection
        /// </summary>
        /// <param name="locationServiceAddress">The GeoLocation Service Hostname (IP Address), including port number</param>
        public GeoLocationService(string locationServiceAddress) {
            // Add the ending slash to be able to GetAsync with the ipAddress directly
            if (!locationServiceAddress.EndsWith("/")) {
                locationServiceAddress += "/";
            }

            this._geoLocationServiceAddress = locationServiceAddress;
            this._httpClient = new HttpClient {
                BaseAddress = new Uri(this._geoLocationServiceAddress)
            };
        }

        /// <summary>
        /// Creates a GeoLocation Service Instance for Dependency Injection (additional constructor for Unit Testing the Service)
        /// </summary>
        /// <param name="locationServiceAddress">The GeoLocation Service Hostname (IP Address), including port number.</param>
        /// <param name="clientHandler">The HttpClientHandler for the HttpClient for mocking responses in Unit Tests.</param>
        public GeoLocationService(string locationServiceAddress, HttpClientHandler clientHandler): this(locationServiceAddress) {
            this._httpClient.Dispose();
            this._httpClient = new HttpClient(clientHandler) {
                BaseAddress = new Uri(this._geoLocationServiceAddress)
            };
        }

        /// <summary>
        /// Geo Location Microservice Http Call with recreation of HttpClient in case of failure
        /// </summary>
        /// <param name="ipAddress">The ip address to locate geographically</param>
        /// <returns>A <see cref="string">string</see> representation of the Json Location Object.</returns>
        private async Task<string> CallGeoLocationServiceAsync(string ipAddress) {
            HttpResponseMessage response;
            string result = null;

            try {
                response = await _httpClient.GetAsync(ipAddress);

                response.EnsureSuccessStatusCode();

                result = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex) {
                lock (_gate) {
                    _httpClient.Dispose(); 
                    _httpClient = new HttpClient {
                        BaseAddress = new Uri(this._geoLocationServiceAddress)
                    };
                }

                result = null;

                Logger.LogExceptionLine("GeoLocationService", "CallGeoLocationServiceAsync", ex.Message, stacktrace: ex.StackTrace);
            }

            return result;
        }

        /// <summary>
        /// Calls the Geo Location Microservice and returns the location description for the provided IP Address. 
        /// </summary>
        /// <param name="ipAddress">The <see cref="string">IP Address</see> to be geographically located by the GeoLocation Microservice.</param>
        /// <returns>A <see cref="string">string</see> representing the json object location description. Does two retries in the case of call failure.</returns>
        public async Task<string> LocateAsync(string ipAddress) {
            int noOfRetries = 2;
            string result = "";

            for (int i = 0; i < noOfRetries; i ++) {
                result = await CallGeoLocationServiceAsync(ipAddress);

                if (result != null) {
                    return result;
                }
            }

            return String.Format(Constants.Location.DefaultGeoLocationResponse, ipAddress);
        }

        public void Dispose() {
            _httpClient.Dispose();
        }

    }

В тестовом классе я хочу написать внутренний класс с именем FakeClientHandler, который расширяет HttpClientHandler и переопределяет метод SendAsync. Другим методом, я думаю, было бы использование фиктивной структуры для имитации HttpClientHandler. Я пока не знаю, как это сделать. Если бы вы могли помочь, я был бы навеки у вас в долгу.

public class GeoLocationServiceTests {

        private readonly IConfiguration _configuration;
        private IGeoLocationService _geoLocationService;
        private HttpClientHandler _clientHandler;

        internal class FakeClientHandler : HttpClientHandler {

            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {

                // insert implementation here

                // insert fake responses here. 
                return (Task<HttpResponseMessage>) null;
            }

        }

        public GeoLocationServiceTests() {
            this._clientHandler = new FakeClientHandler();
            this._clientHandler.UseDefaultCredentials = true;
            this._geoLocationService = new GeoLocationService("http://fakegeolocation.com/json/", this._clientHandler);
        }


        [Fact]
        public async void RequestGeoLocationFromMicroservice() {
            string ipAddress = "192.36.1.252";

            string apiResponse = await this._geoLocationService.LocateAsync(ipAddress);
            Dictionary<string, string> result = JsonConvert.DeserializeObject<Dictionary<string,string>>(apiResponse);

            Assert.True(result.ContainsKey("city"));

            string timeZone;
            result.TryGetValue("time_zone", out timeZone);

            Assert.Equal(timeZone, @"Europe/Stockholm");
        }

        [Fact]
        public async void RequestBadGeoLocation() {
            string badIpAddress = "asjldf";

            string apiResponse = await this._geoLocationService.LocateAsync(badIpAddress);
            Dictionary<string, string> result = JsonConvert.DeserializeObject<Dictionary<string, string>>(apiResponse);

            Assert.True(result.ContainsKey("city"));

            string city;
            result.TryGetValue("city", out city);
            Assert.Equal(city, "NA");

            string ipAddress;
            result.TryGetValue("ip", out ipAddress);
            Assert.Equal(badIpAddress, ipAddress);
        }
    }

@danielmihai HttpMessageHandler фактически является «реализацией». Обычно вы создаете HttpClient, передавая обработчик в конструктор, а затем ваш сервисный уровень зависит от HttpClient.

Использовать универсальную фиктивную платформу сложно, поскольку переопределяемый SendAsync защищен. Я написал mockhttp специально по этой причине (но также потому, что приятно иметь функции насмешек, специфичные для домена.

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

Также есть HttpClient.Helpers как _другая_ библиотека, которая помогает вам имитировать HttpClient .. или, точнее, предоставить поддельный HttpMessageHandler для подделки ответов.

Я понял это, написал что-то вроде этого ... [Я думаю, что справился с «болью»]

internal class FakeClientHandler : HttpClientHandler {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
                if (request.Method == HttpMethod.Get) {
                    if (request.RequestUri.PathAndQuery.Contains("/json/")) {
                        string requestPath = request.RequestUri.PathAndQuery;
                        string[] splitPath = requestPath.Split(new char[] { '/' });
                        string ipAddress = splitPath.Last();

                        // RequestGeoLocationFromMicroservice
                        if (ipAddress.Equals(ipAddress1)) {
                            var response = new HttpResponseMessage(HttpStatusCode.OK);
                            response.Content = new StringContent(response1);

                            return Task.FromResult(response);
                        }

                        // RequestBadGeoLocation
                        if (ipAddress.Equals(ipAddress2)) {
                            var response = new HttpResponseMessage(HttpStatusCode.OK);
                            response.Content = new StringContent(response2);
                            return Task.FromResult(response);
                        }
                    }
                }
                return (Task<HttpResponseMessage>) null;
            }
        }

Все тесты проходят

Пожалуйста, прокомментируйте реализацию, если есть лучшие способы сделать это.

Спасибо!

Добавляю свои мысли к этому более чем годовалому обсуждению.

@СидхартНабар

Во-первых, я заметил, что вы создаете новые экземпляры HttpClient для отправки каждого запроса — это не предполагаемый шаблон проектирования для HttpClient. Создание одного экземпляра HttpClient и его повторное использование для всех ваших запросов помогает оптимизировать пул соединений и управление памятью. Рассмотрите возможность повторного использования одного экземпляра HttpClient. Как только вы это сделаете, вы можете вставить поддельный обработчик только в один экземпляр HttpClient, и все готово.

Где мне избавиться от не внедренного экземпляра, когда я не должен использовать оператор using , но new мой экземпляр HttpClient, например, в конструкторе потребителя? Как насчет HttpMessageHandler, который я создаю для внедрения в экземпляр HttpClient?

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

@richardszalay

Хотя я полностью согласен с тем, что это может быть более легко обнаруживаемым, я не уверен, как это может занять «несколько часов». Поиск в Google «mock httpclient» выдает этот вопрос о переполнении стека в качестве первого результата, в котором два ответа с наивысшим рейтингом (хотя, конечно, не принятый ответ) описывают HttpMessageHandler.

ИМХО, API должен быть интуитивно понятным для использования при просмотре документов. Необходимость задавать вопрос Google или SO, безусловно, не интуитивно понятна, связанный вопрос, возникший задолго до .NET Core, делает актуальность неочевидной (может быть, что-то изменилось между вопросом и выпуском _new_ framework?)

Спасибо всем, кто добавил свои примеры кода и предоставил вспомогательные библиотеки!

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

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

Поскольку http-клиент не имеет интерфейса, мы не можем внедрить его с помощью контейнера.

@dasjestyr Мы можем выполнять внедрение зависимостей без интерфейсов. Интерфейсы допускают множественное наследование; они не являются обязательными для внедрения зависимостей.

Да дело не в ДИ. Как легко/приятно издеваться над этим и т.д.

@shaunluttin Я знаю, что мы можем делать DI без интерфейса, я не имел в виду иначе; Я говорю о классе, который зависит от HttpClient. Без интерфейса для клиента я не могу внедрить фиктивный http-клиент или любой http-клиент (чтобы его можно было протестировать изолированно, не выполняя фактический http-вызов), если только я не заверну его в адаптер, реализующий интерфейс, который я мы определяем, чтобы имитировать HttpClient. Я немного изменил некоторые формулировки для ясности, если вы зациклились на одном предложении.

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

Кроме того, с академической точки зрения, интерфейсы не допускают множественного наследования, они позволяют вам только имитировать его. Вы не наследуете интерфейсы, вы их реализуете — вы не _наследуете_ какую-либо функциональность, реализуя интерфейсы, а только заявляете, что каким-то образом реализовали функциональность. Чтобы имитировать множественное наследование и некоторые уже существующие функции, которые вы написали в другом месте, вы можете реализовать интерфейс и передать реализацию внутреннему члену, который содержит фактическую функциональность (например, адаптеру), который на самом деле является композицией, а не наследованием.

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

Это сердце/ядро этой проблемы -> и это тоже _чисто_ самоуверенно.

Одна сторона забора: поддельный обработчик сообщений, потому что <insert their rational here>
Другая сторона забора: интерфейсы (или даже виртуальные методы, но это все еще немного PITA) из-за <insert their rational here> .

Ну, я имею в виду, что подделки требуют много дополнительной работы для настройки и во многих случаях приводят к тому, что вы пишете целый другой класс только для теста, который на самом деле не добавляет большой ценности. Заглушки хороши тем, что позволяют продолжить тест без реальной реализации. Насмешки - это то, что меня действительно интересует, потому что их можно использовать для утверждений. Например, предположим, что метод в реализации этого интерфейса вызывался x раз с текущими настройками этого теста.

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

Да - здесь полностью с вами согласен 👍 🍰

Пожалуйста, Microsoft, добавьте IHttpClient точку. ИЛИ будьте последовательны и удалите все свои интерфейсы в BCL - IList , IDictionary , ICollection , потому что они все равно «сложны для версии». Черт возьми, также избавьтесь от IEnumerable .

А если серьезно, то вместо аргументов типа «вы можете просто написать свой собственный обработчик» (конечно, но интерфейс проще и чище) и «интерфейс вносит критические изменения» (ну, вы все равно делаете это во всех версиях .net) , просто ДОБАВЬТЕ ИНТЕРФЕЙС, и пусть разработчики решают, как они хотят его тестировать.

@lasseschou IList , IDictionary , ICollection и IEnumerable абсолютно важны из-за большого разнообразия классов, которые их реализуют. IHttpClient будет реализован только HttpClient — чтобы соответствовать вашей логике, каждому отдельному классу в BCL должен быть присвоен тип интерфейса, потому что нам может понадобиться имитировать его.
Мне было бы грустно видеть такой беспорядок.

Если нет серьезного компромисса, IHttpClient — это неправильный уровень абстракции , который нужно высмеивать. Ваше приложение почти наверняка не будет использовать весь спектр возможностей HttpClient. Я бы посоветовал вам обернуть HttpClient в свой собственный сервис с точки зрения конкретных запросов вашего приложения и издеваться над своим собственным сервисом.

@jnm2 jnm2 - Ради интереса, почему вы думаете, что IHttpClient - это неправильный уровень абстракции для насмешек?

Я не хейтирю и не троллю — честный вопрос.

(Люблю учиться и т.д.).

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

Это не абсолют. Если вы создаете веб-браузер, то я мог видеть, как это 1: 1 между тем, что нужно вашему приложению, и тем, что абстрагирует HttpClient. Но если вы используете HttpClient для связи с каким-либо API — чем-либо с более конкретными ожиданиями поверх HTTP — вам понадобится лишь часть того, что может сделать HttpClient. Если вы создаете службу, которая инкапсулирует транспортный механизм HttpClient, и имитируете эту службу, остальная часть вашего приложения может кодировать абстракцию интерфейса, специфичную для API вашей службы, а не кодировать HttpClient.

Кроме того, я придерживаюсь принципа «Никогда не издевайтесь над типом, которым вы не владеете», поэтому YMMV.
См. также http://martinfowler.com/articles/mocksArentStubs.html, сравнивая классическое тестирование и насмешку.

Дело в том, что HttpClient — это такой большой шаг вперед по сравнению с
WebClient, XMLHttpRequest, что он стал предпочтительным способом выполнения HTTP
звонки. Он чистый и современный. Я всегда использую макетные адаптеры, которые говорят
непосредственно в HttpClient, поэтому тестировать мой код легко. Но я хотел бы
также протестируйте эти классы адаптеров. И вот тогда я узнаю: нет
IHttpClient. Возможно, вы правы, это не нужно. Возможно, вы думаете, что это
неправильный уровень абстракции. Но было бы очень удобно не
необходимость обернуть его в еще один класс во всех проектах. я просто не вижу
нет никаких причин не иметь отдельного интерфейса для такого важного класса.

Лассе Скоу
Основатель и генеральный директор

http://mouseflow.com
[email protected]

https://www.facebook.com/mouseflowdotcom/
https://twitter.com/mouseflow
https://www.linkedin.com/company/mouseflow

УВЕДОМЛЕНИЕ О КОНФИДЕНЦИАЛЬНОСТИ: Это сообщение электронной почты, включая все вложения,
для исключительного использования предполагаемым получателем(ями) и может содержать конфиденциальные,
служебная и/или конфиденциальная информация, охраняемая законом. Любой
несанкционированный просмотр, использование, раскрытие или распространение запрещено. если ты
не являются предполагаемым получателем, пожалуйста, свяжитесь с отправителем по электронной почте ответа
и уничтожить все копии исходного сообщения.

12 ноября 2016 г., 14:57, Джозеф Мюссер, [email protected]
написал:

Это не абсолют. Если вы создаете веб-браузер, я мог бы увидеть
как это 1: 1 между тем, что нужно вашему приложению, и тем, что абстрагирует HttpClient. Но
если вы используете HttpClient для общения с каким-либо API - что угодно с
более конкретные ожидания поверх HTTP, вам понадобится только
часть того, что может сделать HttpClient. Если вы создаете сервис, который
инкапсулирует транспортный механизм HttpClient и имитирует эту службу,
остальная часть вашего приложения может кодировать интерфейс, специфичный для API вашего сервиса.
абстракция, а не кодирование для HttpClient.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment -260123552 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/ACnGPp3fSriEJCAjXeAqMZbBcTWcRm9Rks5q9cXlgaJpZM4EPBHy
.

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

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

Исправьте пожалуйста!

Просто наткнулся на это, так что бросаю свои 2c...

Я думаю, что издевательство над HttpClient - это глупость.

Несмотря на то, что есть только один SendAsync для насмешки, есть так много нюансов с HttpResponseMessage , HttpResponseHeaders , HttpContentHeaders и т. д., ваши моки будут быстро пачкаются и, вероятно, тоже ведут себя неправильно.

Мне? Я использую OwinHttpMessageHandler для

  1. Выполнение приемочных тестов HTTP вне службы.
  2. Внедрите _Dummy_ HTTP-сервисы в компоненты, которые хотят делать исходящие HTTP-запросы к указанным сервисам. То есть мой компонент имеет необязательный параметр ctor для HttpMessageHandler .

Я уверен, что есть обработчик MVC6, который делает подобное....

_Жизнь прекрасна._

@damianh : Почему люди, использующие ядро ​​​​dotnet, должны что-то делать с Owin, чтобы протестировать такой класс, как HttpClient? Это может иметь большое значение для конкретного варианта использования (AspCore, размещенный на OWIN), но это не так уж часто.

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

О, _testing_ HttpClient... думал, что эта тема тестирует вещи, которые имеют
зависимость от HttpClient. Не обращай на меня внимания. :)

5 декабря 2016 г., 19:56, «brphelps» [email protected] написал:

@damianh https://github.com/damianh : Почему люди должны использовать ядро ​​​​dotnet
нужно что-то делать с Owin, чтобы протестировать такой класс, как HttpClient? Это может
имеют большой смысл для конкретного варианта использования (AspCore, размещенный на OWIN), но
это не кажется таким общим.

Кажется, это действительно смешивает заботу о том, «что является наиболее общим
способ модульного тестирования HttpClient, который также соответствует ожиданиям пользователей»
с "как мне интеграционный тест" :).


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-264942511 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AADgXJxunxBgFGLozOsisCoPqjMoch48ks5rFF5vgaJpZM4EPBHy
.

@damianh - это вещи, которые имеют зависимость, но ваше решение, похоже, также включает в себя множество других зависимостей. Я представляю себе библиотеку классов, в которой используется переданный экземпляр HttpClient (или экземпляр DI) — причем здесь Оуин?

Это не самое интересное, что я пытался показать (что
также будет работать на .net core/netstandard), поэтому, пожалуйста, не зацикливайтесь на этом.

Интересно, что HttpMessageHandler создает внутрипроцессное
HTTP-запрос в памяти к фиктивным конечным точкам HTTP.

TestServer Aspnet Core, как и предшествующий ему katana, работает одинаково.
способ.

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

Насмешка, особая форма тестового двойника, — это то, что я все еще советую.
против для HttpClient. Таким образом, нет необходимости в IHttpClient.

5 декабря 2016 г., 20:15, «brphelps» [email protected] написал:

@damianh https://github.com/damianh -- это вещи, которые имеют
зависимость, но ваше решение, похоже, включает много других зависимостей
на картинке тоже. Я представляю себе библиотеку классов, которая использует
передается в экземпляре HttpClient (или в DI) - Где Овин входит в
изображение?


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-264947538 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AADgXEESfQj9NnKLo8LucFLyajWtQXKBks5rFGK8gaJpZM4EPBHy
.

@damianh - Полностью понимаю, о чем вы говорите, интересно то, что - но людям не нужны фиктивные конечные точки HTTP с умным макетом - например, библиотека MockHttp @richardszalay.

По крайней мере, не для модульного тестирования :).

Это зависит. Варианты хорошие :)

5 декабря 2016 г., 20:39, «brphelps» [email protected] написал:

@damianh https://github.com/damianh -- полностью понимаю, о чем вы
говоря, что интересно, но людям не нужен фиктивный HTTP
конечные точки с умным макетом - например, @richardszalay
Библиотека MockHttp https://github.com/richardszalay .

По крайней мере, не для модульного тестирования :).


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-264954210 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AADgXOl4UEDGYCVLpbvwhueaK52VtjH6ks5rFGhlgaJpZM4EPBHy
.

@damianh сказал:
Варианты хорошие :)

Да - могу полностью согласиться с этим 👍 Которого, ИМО .. у нас сейчас нет 😢 **

Я полностью понимаю рациональность создания и внедрения собственного HMH. Это то, что я делаю прямо сейчас. _К сожалению_, мне нужно создать 2-й ctor (или указать необязательный параметр ctor) _для модульного тестирования_. Просто подумайте об этом предложении еще раз -> я создаю вполне возможную ПОЛЬЗОВАТЕЛЬСКУЮ ОПЦИЮ ... которая на самом деле предназначена только для того, чтобы модульный тест мог _делать вещи_. Конечный пользователь/потребитель моего класса никогда не будет _реально_ использовать эту опцию ctor... которая для меня пахнет.

Вернемся к вопросу о вариантах -> сейчас я не чувствую, что у нас есть один. Мы _должны_ узнать_ о внутреннем устройстве HttpClient . Буквально открыть капот и заглянуть под капот. (Мне посчастливилось получить TL;DR от Sir Fowler The Awesome ™️, что значительно сократило время моих исследований).

Итак, с вашей точки зрения об отсутствии интерфейса, не могли бы вы привести несколько примеров, когда интерфейс был бы плохой вещью, пожалуйста? и т. д. Помните -> я всегда продвигал интерфейс как _вариант_ для небольших/простых и средних примеров... не 100% всех примеров.

Я не пытаюсь троллить или атаковать тебя, Дамиан, просто пытаюсь понять.

* Ninja Edit: технически у нас действительно есть варианты -> мы ничего не можем делать/создавать свои собственные обертки/и т. д. Я имел в виду: не так много вариантов, которые упрощают вещи, *из коробки .

Привет, @PureKrome , не беспокойся, просто делюсь опытом.

Термин «модульный тест» использовался несколько раз, и давайте задумаемся об этом на минуту. Итак, мы на одной странице, я предполагаю, что у нас есть FooClass , у которого есть зависимость ctor от HttpClient (или HMH ), и мы хотим _mock_ HttpClient .

На самом деле мы говорим здесь: « FooClass может сделать HTTP-запрос, используя любой URL-адрес (протокол/хост/ресурс), используя любой метод, любой тип контента, любое кодирование передачи, любое кодирование контента, _любой заголовки запросов_ и может обрабатывать любой тип содержимого ответа, любой код состояния, файлы cookie, перенаправления, кодировку передачи, заголовки управления кешем и т . д., а также тайм-ауты сети, исключения отмены задачи и т. д.».

HttpClient подобен Объекту Бога ; вы можете многое сделать с ним. HTTP также является удаленным сервисным вызовом, выходящим за пределы вашего процесса.

Это действительно модульное тестирование? Скажи прямо сейчас :)

** Ninja Edit тоже: если вы хотите сделать модуль FooClass пригодным для модульного тестирования, где он хочет получить json с сервера, у него будет зависимость ctor от Func<CancellationToken, Task<JObject>> или подобного.

не могли бы вы привести несколько примеров, когда интерфейс был бы плохой вещью, пожалуйста

Не то сказал! Я сказал, что IHttpClient не будет полезен, потому что A) он не будет заменен альтернативной конкретной реализацией и B) проблема объекта Бога, как уже упоминалось.

Другой пример? IAssembly .

@damianh - Статус «Божественный класс» не беспокоит звонящих. Вызывающая сторона не несет ответственности за то, чтобы HttpClient был/не был божественным классом. Они знают, что это объект, который они используют для HTTP-вызовов :).

Итак, если предположить, что мы согласны с тем, что инфраструктура .NET открыла нам класс God... как мы можем уменьшить наше беспокойство? Как мы можем лучше всего инкапсулировать это? Это просто — используйте традиционную фиктивную структуру (отличный пример — Moq) и полностью изолируйте реализацию HttpClient, потому что людям, использующим ее, просто все равно, как она устроена внутри . Мы заботимся только о том, как мы его используем. Проблема здесь в том, что традиционный подход не работает, и в результате вы видите, что люди придумывают все более и более интересные решения для решения проблемы :).

Некоторые правки для грамматики и ясности :D

Полностью согласен с @damianh в том, что HttpClient это что-то вроде сверхбожественного класса.

Я также не понимаю, зачем мне снова знать обо всех внутренних, потому что может быть так много изменений_ того, что входит и что выходит.

Например: я хочу получить акции из какого-то API. https://api.stock-r-us.com/aapl . Результатом является некоторый полезный код JSON.

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

  • неверный URL-адрес конечной точки?
  • неправильная схема?
  • отсутствуют определенные заголовки?
  • неправильные заголовки
  • неправильная полезная нагрузка запроса (может быть, это POST с каким-то телом?)
  • сетевая ошибка/тайм-аут/и т.д.
    ...

Итак, я понимаю, что если я хочу проверить эти вещи, HMH — отличный способ сделать это, потому что он просто захватывает _real_ HMH и заменяет то, что я сказал, чтобы вернуться.

Но что, если я хочу (на свой страх и риск) проигнорировать все эти вещи и просто сказать:
_Представьте, что я хочу позвонить и конечную точку, все в порядке, и я получаю хороший результат полезной нагрузки json._
Должен ли я все еще учиться и беспокоиться обо всех этих вещах, чтобы сделать что-то настолько простое?

Или... это плохой способ мышления. Мы никогда не должны так думать, потому что это слишком хакерски? слишком дешево? слишком склонен к ошибкам?

В большинстве случаев я просто звоню в конечную точку. Я не думаю о таких вещах, как заголовки, управление кешем или сетевые ошибки. Обычно я просто думаю о ГЛАГОЛ + URL (и ПОЛЕЗНОЙ НАГРУЗКЕ, если требуется), а затем о РЕЗУЛЬТАТЕ. У меня есть обработка ошибок вокруг этого, чтобы поймать все-все-вещи :tm: а затем признать поражение, потому что случилось что-то плохое :(

Так что либо я смотрю на проблему неправильно (т. е. я все еще не могу избавиться от своего статуса n00b), либо этот класс God причиняет нам кучу горя при модульном тестировании.

HTTP также является удаленным сервисным вызовом, выходящим за пределы вашего процесса.
Это действительно модульное тестирование? Скажи прямо сейчас :)

Для меня HttpClient — это _зависимая служба_. Меня не волнует, что на самом деле делает сервис, я классифицирую его как _что-то, что делает что-то, что является внешним по отношению к моему коду_. Поэтому я не хочу, чтобы он делал что-то с внешним питанием, поэтому я хочу контролировать это, захватывать его и определять результаты этих сервисных вызовов. Я все еще пытаюсь протестировать _мою_ единицу работы. Моя маленькая логика. Конечно, это может зависеть от других вещей. Это все еще модульный тест, если я могу контролировать всю вселенную, эта часть логики существует внутри _и_ я не полагаюсь/не интегрирую другие сервисы.

** Другое редактирование: Или, может быть, - если HttpClient на самом деле является объектом Бога, может быть, разговор должен быть о том, чтобы свести его к чему-то... лучшему? А может быть, это вовсе не Божественный Объект? (просто пытаюсь открыть дискуссию)..

К сожалению, мне нужно создать второй ctor (или указать необязательный параметр ctor) для модульного тестирования. Просто подумайте об этом предложении еще раз -> я создаю вполне возможную ПОЛЬЗОВАТЕЛЬСКУЮ ОПЦИЮ ... которая на самом деле предназначена только для того, чтобы модульный тест мог что-то делать. Конечный пользователь/потребитель моего класса никогда не будет использовать эту опцию ctor... которая для меня пахнет

Я не уверен, что вы пытаетесь сказать здесь:

«Мне не нравится иметь второй ctor, который принимает HttpMessageHandler»… так что ваши тесты оберните его в HttpClient

«Я хочу выполнить модульное тестирование, не инвертируя свою зависимость от HttpClient»… сложно?

@richardszalay - я думаю, я понимаю, что @PureKrome не хочет изменять свой код таким образом, чтобы не улучшить дизайн, только для поддержки чрезмерно самоуверенной стратегии тестирования. Не поймите меня неправильно, я не жалуюсь на вашу библиотеку, единственная причина, по которой мне нужно лучшее решение, заключается в том, что я больше ожидаю, что ядро ​​​​dotnet не потребует его =).

Я не думаю об этом с точки зрения MockHttp, а вообще о тестировании/насмешке.
Инвертированные зависимости являются фундаментальной необходимостью для модульного тестирования в .NET, и это обычно включает внедрение ctor. Я не понимаю, какой будет предложенная альтернатива.

Конечно, если бы HttpClient реализовал IHttpClient, все равно был бы конструктор, который его принял...

Конечно, если бы HttpClient реализовал IHttpClient, все равно был бы конструктор, который его принял...

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

Проигнорируйте мой комментарий 'ctor', пожалуйста, и продолжайте обсуждение, если кто-то еще не ушел от скуки 😄

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

Допустим, у меня есть класс FooClass , в который внедряется конструктор зависимостей. Теперь я хочу выполнить модульное тестирование методов на FooClass , то есть я указываю входные данные для своего метода, я изолирую FooClass как можно лучше от его среды и проверяю результат на соответствие ожидаемому значению. . Во всем этом конвейере я не хотел бы заботиться о внутренностях моего зависимого объекта; для меня было бы лучше, если бы я мог просто указать тип среды, в которой должен жить мой FooClass : я хотел бы имитировать зависимость от моей тестируемой системы таким образом, чтобы она вел себя именно так, как мне нужно чтобы мои тесты работали.

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

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

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

«Для меня HttpClient является зависимой службой. Меня не волнует, что эта служба на самом деле делает, я классифицирую ее как что-то, что делает что-то, что является внешним по отношению к моей кодовой базе». — PureKrome

«Теперь я должен заботиться о том, как реализован HttpClient, потому что мне нужно указать зависимость (HttpMessageHandler) для моей внедренной зависимости (HttpClient) для моей тестируемой системы». — Таоден

«Я собирался спросить то же самое, учитывая, что это почти идеальный уровень абстракции, поскольку это клиент». -- дашестыр

«HttpMessageHandler фактически является «реализацией». Обычно вы создаете HttpClient, передавая обработчик в конструктор, а затем ваш сервисный уровень зависит от HttpClient». -- Ричардзалай

«Но ключевая проблема останется — вам нужно будет определить поддельный обработчик и вставить его ниже объекта HttpClient». -- Сидхарт Набар

«HttpClient имеет множество вариантов модульного тестирования. Он не запечатан, и большинство его членов являются виртуальными. У него есть базовый класс, который можно использовать для упрощения абстракции, а также тщательно разработанная точка расширения в HttpMessageHandler», — ericsstj

«Если API не поддается макетированию, мы должны это исправить. Но это не имеет ничего общего с интерфейсами и классами. Интерфейс ничем не отличается от чистого абстрактного класса с точки зрения макетирования для всех практических целей». -- Кшиштоф Цвалина

«При всем уважении, если вариант заключается в том, чтобы написать собственный обработчик поддельных сообщений, что неочевидно без копания в коде, то это очень высокий барьер, который сильно разочарует многих разработчиков». -- ктотолкин

Это действительно модульное тестирование? Скажи прямо сейчас :)

Понятия не имею. Как выглядит тест?

Я думаю, что заявление является чем-то вроде соломенного человека. Дело в том, чтобы имитировать зависимость, которая является http-клиентом, чтобы код мог работать изолированно от этих зависимостей... другими словами, фактически не делать HTTP-вызов. Все, что нам нужно сделать, это убедиться, что ожидаемые методы интерфейса вызываются в правильных условиях. Вопрос о том, должны ли мы выбирать более легкую зависимость, кажется совершенно другим.

HttpClient — худшая зависимость. Буквально утверждается, что
зависимый может "звонить в Интернет". Конечно, вы все будете издеваться над TCP. Хех

7 декабря 2016 г., 5:34 утра, «Джереми Стаффорд» [email protected] написал:

Это действительно модульное тестирование? Скажи прямо сейчас :)

Понятия не имею. Как выглядит тест?

Я думаю, что заявление является чем-то вроде соломенного человека. Суть в том, чтобы издеваться над
зависимость, которая является http-клиентом. Все, что мы будем делать, это убедиться, что
ожидаемые методы интерфейса вызывались под правым
условия. Казалось бы, должны ли мы выбрать более легкую зависимость.
будет совсем другое обсуждение.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-265353526 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AADgXPL39vnUbWrUuUBtfmbjhk5IBkxHks5rFjdqgaJpZM4EPBHy
.

Это в основном не следует модульному тестированию 101 - получение несвязанного кода зависимостей из ваших тестов =).

Лично я не думаю, что это правильная интерпретация этой конкретной практики. Мы не пытаемся проверить зависимость, мы пытаемся проверить, как наш код взаимодействует с этой зависимостью , поэтому это не является не связанным. И, проще говоря, если я не могу запустить модульный тест своего кода без вызова фактической зависимости, то это не изолированное тестирование. Например, если этот модульный тест был запущен на сервере сборки без доступа к Интернету, то тест, скорее всего, сломается — отсюда и желание поиздеваться над ним. Вы можете удалить зависимость от HttpClient, но это просто означает, что он попадет в какой-то другой класс, который вы пишете, который выполняет вызов - может быть, я говорю об этом классе? Ты не знаешь, потому что никогда не спрашивал. Так должна ли моя доменная служба зависеть от HttpClient? Нет, конечно нет. Это было бы абстрагировано для какого-то другого абстрактного сервиса, и вместо этого я бы издевался над этим. Итак, на уровне, который внедряет любую абстракцию, которую я придумаю, где-то вверх по цепочке _something_ будет знать о HttpClient, и это что-то будет обернуто чем-то другим, что я напишу, и это что-то еще будет тем, что я Я все еще собираюсь написать модульный тест для.

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

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

Но теперь позвольте мне дать задний ход. Чем больше я смотрю на него, тем больше он выглядит слишком толстым для макетного интерфейса (что уже было сказано). Я думаю, что это может быть ментальная вещь, происходящая от имени «Клиент». Я не говорю, что я думаю, что это неправильно, но, скорее, это немного не соответствует другим реализациям «Клиента», которые в наши дни можно увидеть в дикой природе. Сегодня мы видим много «клиентов», которые на самом деле являются фасадами службы, поэтому, когда кто-то видит имя Http CLIENT, они могут подумать то же самое, и почему бы вам не издеваться над службой, верно?

Теперь я вижу, что обработчик — это та часть кода, которая имеет значение для кода, поэтому в будущем я буду проектировать вокруг него.

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

Хорошо, я продан. Мне больше не нужен интерфейс HttpClient для насмешек.

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

Сегодня наткнулся на это: http://www.davesquared.net/2011/04/dont-mock-types-you-dont-own.html

Практически невозможно поделиться экземпляром HttpClient с любым реальным приложением, если вам нужно отправлять разные заголовки HTTP для каждого запроса (что имеет решающее значение при общении с правильно спроектированными веб-службами RESTful). В настоящее время HttpRequestHeaders DefaultRequestHeaders привязан к экземпляру, а не к его вызову, что эффективно делает его с сохранением состояния. Вместо этого {Method}Async() должен принять это, что сделает HttpClient без состояния и действительно пригодным для повторного использования.

@abatishchev Но вы можете указать заголовки для каждого HttpRequestMessage.

@richardszalay Я не говорю, что это совершенно невозможно, я говорю, что HttpClient не был разработан для этой цели. Ни один из {Method}Async() не принимает HttpRequestMethod , только SendAsync() $ принимает. Но какова цель остальных тогда?

Но какова цель остальных тогда?

Удовлетворение 99% потребностей.

Означает ли это, что установка заголовков составляет 1% вариантов использования? Я сомневаюсь.

В любом случае это не будет проблемой, если эти методы имеют перегрузку, принимающую HttpResponseMessage.

@abatishchev Я в этом не сомневаюсь, но в любом случае я бы написал методы расширения, если бы оказался в вашем сценарии.

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

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

@dasjestyr Пробовали ли вы создать интерфейс для HttpClient (что похоже на создание для него оболочки) вместо интерфейсов для HttpMessageHandler или HttpRequestMessage ..

/мне любопытно.

@PureKrome Я набросал для него интерфейс и быстро понял, что это бессмысленно. HttpClient на самом деле просто абстрагирует кучу вещей, которые не имеют значения в контексте модульного тестирования, а затем вызывает обработчик сообщений (что несколько раз упоминалось в этом потоке). Я также пытался создать для него оболочку, и это просто не стоило работы, необходимой для ее реализации или распространения практики (например, «йоу, все делают это вместо прямого использования HttpClient»). НАМНОГО проще просто сосредоточиться на обработчике, так как он дает вам все необходимое и буквально состоит из одного метода.

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

Однако я думаю, что интерфейс обработчика может быть полезен, но на этом придется остановиться. Затем можно настроить вашу фиктивную структуру для возврата предварительно определенных экземпляров HttpResponseMessage.

Интересно. Я обнаружил (личное предубеждение?), что моя вспомогательная библиотека отлично работает при использовании (фальшивого) конкретного обработчика сообщений ... по сравнению с некоторыми элементами интерфейса на этом низком уровне.

Я бы все же предпочел не писать эту библиотеку и не использовать ее :)

Не вижу проблемы в создании небольшой библиотеки для сборки фейков. Я мог бы сделать это, когда мне скучно от нечего делать. Весь мой http-материал уже абстрагирован и протестирован, поэтому на данный момент мне он не нужен. Я просто не вижу смысла в обертывании HttpClient для модульного тестирования. Подделка обработчика — это все, что вам действительно нужно. Расширение функционала — это совершенно отдельная тема.

Когда большая часть кодовой базы тестируется с использованием фиктивных интерфейсов, удобнее и последовательнее, когда остальная часть кодовой базы тестируется таким же образом. Поэтому я хотел бы видеть интерфейс IHttpClient. Например, IFileSystemOperarions из ADL SDK или IDataFactoryManagementClient из SDK управления ADF и т. д.

Я все еще думаю, что вы упускаете из виду то, что HttpClient не нужно издеваться, только обработчик. Настоящая проблема заключается в том, как люди смотрят на HttpClient. Это не какой-то случайный класс, который следует обновлять всякий раз, когда вы думаете о вызове в Интернет. На самом деле лучше всего повторно использовать клиент во всем приложении — это очень важно. Разделите эти две вещи, и это обретет больше смысла.

Кроме того, ваши зависимые классы не должны заботиться о HttpClient, а только о данных, которые он возвращает, которые поступают от обработчика. Подумайте об этом так: собираетесь ли вы когда-нибудь заменить реализацию HttpClient чем-то другим? Возможно... но маловероятно. Вам никогда не нужно менять способ работы клиента, так зачем его абстрагировать? Обработчик сообщения является переменной. Вы хотели бы изменить способ обработки ответов, но не то, что делает клиент. Даже конвейер в WebAPI ориентирован на обработчик (см. делегирование обработчика). Чем больше я это говорю, тем больше начинаю думать, что .Net должен сделать клиент статическим и управлять им за вас... но я имею в виду... что угодно.

Помните, для чего нужны интерфейсы. Они не для тестирования — это был просто умный способ использовать их. Создавать интерфейсы исключительно для этой цели смешно. Microsoft предоставила вам то, что вам нужно, чтобы отделить поведение обработки сообщений, и это отлично подходит для тестирования. На самом деле, HttpMesageHandler является абстрактным, поэтому я думаю, что большинство имитирующих фреймворков, таких как Moq, все еще будут работать с ним.

Хех @dasjestyr - Я тоже думаю, что вы, возможно, упустили важный момент моего обсуждения.

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

Я надеюсь, что основной смысл этой эпической ветки заключается в том, чтобы подчеркнуть, что... при использовании HttpClient в приложении тестировать с ним PITA.

Статус-кво «иди изучай сантехнику HttpClient, что приведет тебя к HttpMessageHandler и т. д.» — это плохая ситуация. Нам не нужно делать это для многих других библиотек и т.д.

Поэтому я надеялся, что можно что-то сделать, чтобы облегчить этот PITA.

Да, PITA — это мнение, я полностью согласен с этим. Некоторые люди вообще не думают, что это PITA и т. д.

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

Согласованный! Но до недавнего времени об этом не было известно, что могло привести к отсутствию документации, обучения или чего-то еще.

Кроме того, ваши зависимые классы не должны заботиться о HttpClient, а только о данных, которые он возвращает.

Ага - согласен.

который исходит от обработчика.

что? Хм? О, теперь ты просишь меня открыть капот и узнать о сантехнике? ... См. выше снова и снова.

Подумайте об этом так: собираетесь ли вы когда-нибудь заменить реализацию HttpClient чем-то другим?

Нет.

.. так далее ..

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

Я уже давно использую свою вспомогательную библиотеку, которая представляет собой просто прославленный способ использования пользовательского обработчика сообщений. Здорово! Он работает и работает отлично. Я просто думаю, что это могло бы быть _all_ выставленным немного лучше ... и это то, что я надеюсь, что эта тема действительно попадает в точку.

РЕДАКТИРОВАТЬ: форматирование.

(Я только что заметил грамматическую ошибку в названии этого выпуска и теперь не могу ее развидеть)

И это была моя _реально_ долгая игра :)

КЭД

(на самом деле - так неловко 😊 )

РЕДАКТИРОВАТЬ: часть меня не хочет это редактировать... из-за ностальгии....

(Я только что заметил грамматическую ошибку в названии этого выпуска и теперь не могу ее развидеть)

Я знаю! Такой же!

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

что?

Вы хотя бы смотрели на HttpMessageHandler? Это абстрактный класс, который буквально представляет собой единый метод, который принимает HttpRequestMessage и возвращает HttpResponseMessage, предназначенный для перехвата поведения отдельно от всего низкоуровневого транспортного мусора, что именно то, что вам нужно в модульном тесте. Чтобы подделать это, просто реализуйте это. Входящее сообщение — это то, что вы отправили HttpClient, а исходящий ответ зависит от вас. Например, если я хочу знать, что мой код правильно работает с телом json, то просто пусть ответ возвращает тело json, которое вы ожидаете. Если вы хотите увидеть, правильно ли он обрабатывает 404, то пусть он вернет 404. Это не более просто. Чтобы использовать обработчик, просто отправьте его в качестве аргумента ctor для HttpClient. Вам не нужно тянуть какие-либо провода или изучать внутреннюю работу чего-либо.

Я надеюсь, что основной смысл этой эпической ветки заключается в том, чтобы подчеркнуть, что... при использовании HttpClient в приложении тестировать с ним PITA.

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

Я думаю, что HttpClient в пространстве имен Windows действительно разделил концепцию обработчика на filter , но он делает то же самое. Я думаю, что обработчик/фильтр в этой реализации на самом деле имеет интерфейс, что я и предлагал ранее.

что?
Вы хотя бы смотрели на HttpMessageHandler?

Изначально нет, из-за этого:

var payloadData = await _httpClient.GetStringAsync("https://......");

это означает, что открытые методы на HttpClient действительно хороши и делают вещи _действительно_ простыми :) Ура! Быстро, просто, работает, ВЫИГРЫВАЙТЕ!

Итак, теперь давайте используем его в тесте ... и теперь нам нужно потратить время на изучение того, что происходит внутри ... что приводит нас к изучению HttpMessageHandler и т. д.

Опять же, я продолжаю повторять это => это дополнительное изучение сантехники HttpClient (т.е. HMH и т. д.) является PITA. После того, как вы _обучились_ .. да, я знаю, как его использовать и т. д. ... хотя я также не люблю продолжать его использовать, но мне нужно, чтобы имитировать удаленные вызовы на сторонние конечные точки. Конечно, это самоуверенно. Мы можем только соглашаться не соглашаться. Так что, пожалуйста, я знаю о HMH и как его использовать.

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

Да, люди со мной не согласны. Нет проблем. Кроме того, люди согласны со мной тоже. Опять же, никаких проблем.

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

Хорошо - я с уважением не согласен с вами здесь (что нормально). Опять разные мнения.

Но серьезно. Эта ветка продолжается так долго. Я действительно продвинулся к настоящему времени. Я сказал свою часть, некоторые люди говорят ДА! некоторые сказали НЕТ! Ничего не изменилось, и я просто… принял статус-кво и смирился со всем.

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

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

Как в сторону:

Изначально нет, из-за этого:

var payloadData = await _httpClient.GetStringAsync("https://...");

это означает, что открытые методы в HttpClient действительно хороши и упрощают работу :) Ура! Быстро, просто, работает, ВЫИГРЫВАЙТЕ!

Я бы сказал, что с точки зрения SOLID этот интерфейс в любом случае неуместен. Клиент IMO, запрос, ответ - это 3 разные обязанности. Я могу оценить удобные методы на клиенте, но они способствуют тесной связи, комбинируя запросы и ответы с клиентом, но это личное предпочтение. У меня есть некоторые расширения для HttpResponseMessage, которые выполняют то же самое, но несут ответственность за чтение ответа с ответным сообщением. По моему опыту, в больших проектах «Простой» никогда не бывает «Простым» и почти всегда заканчивается BBOM. Впрочем, это уже совсем другой разговор :)

Теперь, когда у нас есть новый репозиторий специально для обсуждения дизайна, продолжайте обсуждение на https://github.com/dotnet/designs/issues/9 или откройте новую проблему на https://github.com/dotnet/designs .

Спасибо.

Возможно, это можно было бы рассмотреть ниже как решение только для целей тестирования:

открытый класс GeoLocationServiceForTest: GeoLocationService, IGeoLocationService
{
public GeoLocationServiceForTest (что-то: что-то, HttpClient httpClient): база (что-то)
{
base._httpClient = httpClient;
}
}

В итоге я использовал MockHttp @richardszalay .
Спасибо, Ричард, ты спас мой день!

Хорошо, я могу жить с HttpClient без интерфейса. Но быть вынужденным реализовать класс, наследующий HttpMessageHandler , потому что SendAsync защищен, просто отстой.

Я использую NSubstitute, и это не работает:

var httpMessageHandler = 
    Substitute.For<HttpMessageHandler>(); // cannot be mocked, not virtual nor interfaced
httpMessageHandler.SendAsync(message, cancellationToken)
    .Return(whatever); // doesn't compile, it's protected
var httpClient = new HttpClient(httpMessageHandler)

Действительно разочаровывает.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

omajid picture omajid  ·  3Комментарии

sahithreddyk picture sahithreddyk  ·  3Комментарии

btecu picture btecu  ·  3Комментарии

jchannon picture jchannon  ·  3Комментарии

bencz picture bencz  ·  3Комментарии