Runtime: Eine einfache Möglichkeit, eine httpClient.GetAsync(..)-Methode für Komponententests zu verspotten?

Erstellt am 4. Mai 2015  ·  157Kommentare  ·  Quelle: dotnet/runtime

System.Net.Http wurde jetzt in das Repo hochgeladen :smile: :tada: :balloon:

Wann immer ich dies in einem Dienst verwendet habe, funktioniert es großartig, macht es jedoch schwierig, Einheiten zu testen => Meine Einheitentests wollen diesen wirklichen Endpunkt nie erreichen.

Vor Ewigkeiten habe ich @davidfowl gefragt, was wir tun sollen? Ich hoffe, ich paraphrasiere ihn und zitiere ihn nicht falsch - aber er schlug vor, dass ich einen Message-Handler vortäuschen (dh HttpClientHandler ), das verkabeln usw.

Aus diesem Grund habe ich eine Hilfsbibliothek namens HttpClient.Helpers erstellt , die mir bei der Ausführung meiner Komponententests hilft.

Das funktioniert also ... aber es fühlt sich _sehr_ chaotisch und ... kompliziert an. Ich bin mir sicher, dass ich nicht die erste Person bin, die sicherstellen muss, dass meine Komponententests keinen echten Aufruf an einen externen Dienst ausführen.

Gibt es einen einfacheren Weg? Wie ... können wir nicht einfach eine IHttpClient -Schnittstelle haben und diese in unseren Dienst einfügen?

Design Discussion area-System.Net.Http test enhancement

Hilfreichster Kommentar

Hallo @SidarthNabar - Danke, dass du mein Problem gelesen hast, ich weiß es auch sehr zu schätzen.

Erstellen Sie eine neue Handler-Klasse

Du hast gerade meine Frage beantwortet :)

Das ist auch ein großer Tanz zum Wackeln, nur um meinen echten Code zu bitten, nicht _das Netzwerk zu treffen_.

Ich habe sogar ein HttpClient.Helpers -Repo- und Nuget-Paket erstellt ... nur um das Testen zu vereinfachen! Szenarien wie der Happy-Pfad oder eine vom Netzwerkendpunkt ausgelöste Ausnahme ...

Das ist das Problem -> können wir das nicht alles tun und ... stattdessen nur eine Scheinmethode?

Ich werde versuchen, es per Code zu erklären.

Ziel: Laden Sie etwas aus dem Internet herunter.

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

Schauen wir uns zwei Beispiele an:

Gegeben eine Schnittstelle (falls vorhanden):

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

Mein Code könnte jetzt so aussehen...

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");
        }
        ....
    } 
}

und die Testklasse

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

Yay! fertig.

Ok, jetzt mit dem aktuellen Weg ...

  1. Erstellen Sie eine neue Handler-Klasse - ja, eine Klasse!
  2. Fügen Sie den Handler in den Dienst ein
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");
        }
        ....
    } 
}

Aber der Schmerz ist jetzt, dass ich eine Klasse machen muss. Jetzt hast du mich verletzt.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

Und diese Klasse fängt ziemlich einfach an. Bis ich, wenn ich mehrere GetAsync calls habe (also muss ich mehrere Handler-Instanzen bereitstellen?) -oder- mehrere httpClients in einem einzigen Dienst. Verwenden wir außerdem Dispose des Handlers oder verwenden wir ihn wieder, wenn wir mehrere Aufrufe im selben Logikblock ausführen (was eine ctor-Option ist)?

z.B.

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;
    }
    ....
}

das kann so viel einfacher gemacht werden..

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");

sauber sauber sauber :)

Dann - da ist das nächste Bit: Auffindbarkeit

Als ich anfing, MS.Net.Http.HttpClient zu verwenden, war die API ziemlich offensichtlich :+1: Ok - String holen und asynchron machen ... einfach ...

... aber dann drücke ich auf Testen .... und jetzt soll ich etwas über HttpClientHandlers lernen? Ähm warum? Ich bin der Meinung, dass dies alles unter der Decke verborgen werden sollte und ich mich nicht um all diese Implementierungsdetails kümmern muss. Es ist zu viel! Du sagst, dass ich anfangen sollte, in die Kiste zu schauen und etwas über die Klempnerarbeit zu lernen ... was weh tut :cry:

Das alles macht es komplexer, als es sein sollte, IMO.


Helfen Sie mir, Microsoft – Sie sind meine einzige Hoffnung.

Alle 157 Kommentare

HttpClient ist nichts anderes als eine Abstraktionsschicht über einer anderen HTTP-Bibliothek. Einer der Hauptzwecke besteht darin, Ihnen zu ermöglichen, das Verhalten (Implementierung) zu ersetzen, einschließlich, aber nicht beschränkt auf die Möglichkeit, deterministische Komponententests zu erstellen.

In anderen Arbeiten dient HttpClient selbst sowohl als „echtes“ als auch als „scheinbares“ Objekt, und HttpMessageHandler ist das, was Sie auswählen, um die Anforderungen Ihres Codes zu erfüllen.

Aber wenn es sich so anfühlt, als ob sooo viel Arbeit erforderlich ist, um den Message-Handler einzurichten, wenn (es fühlt sich so an, als ob) dies mit einer netten Schnittstelle gehandhabt werden könnte? Verstehe ich die Lösung total falsch?

@PureKrome - danke, dass du das zur Diskussion gebracht hast. Können Sie bitte erläutern, was Sie mit "es ist so viel Arbeit erforderlich, um den Nachrichtenhandler einzurichten" meinen?

Eine Möglichkeit, HttpClient zu testen, ohne das Netzwerk zu treffen, ist -

  1. Erstellen Sie eine neue Handler-Klasse (z. B. FooHandler), die von HttpMessageHandler abgeleitet ist
  2. Implementieren Sie die SendAsync-Methode gemäß Ihren Anforderungen – treffen Sie nicht das Netzwerk/protokollieren Sie die Anfrage-Antwort/etc.
  3. Übergeben Sie eine Instanz dieses FooHandlers an den Konstruktor des HttpClient:
    var handler = new FooHandler();
    var client = new HttpClient(handler);

Ihr HttpClient-Objekt verwendet dann Ihren Handler anstelle des eingebauten HttpClientHandler.

Danke,
Sid

Hallo @SidarthNabar - Danke, dass du mein Problem gelesen hast, ich weiß es auch sehr zu schätzen.

Erstellen Sie eine neue Handler-Klasse

Du hast gerade meine Frage beantwortet :)

Das ist auch ein großer Tanz zum Wackeln, nur um meinen echten Code zu bitten, nicht _das Netzwerk zu treffen_.

Ich habe sogar ein HttpClient.Helpers -Repo- und Nuget-Paket erstellt ... nur um das Testen zu vereinfachen! Szenarien wie der Happy-Pfad oder eine vom Netzwerkendpunkt ausgelöste Ausnahme ...

Das ist das Problem -> können wir das nicht alles tun und ... stattdessen nur eine Scheinmethode?

Ich werde versuchen, es per Code zu erklären.

Ziel: Laden Sie etwas aus dem Internet herunter.

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

Schauen wir uns zwei Beispiele an:

Gegeben eine Schnittstelle (falls vorhanden):

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

Mein Code könnte jetzt so aussehen...

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");
        }
        ....
    } 
}

und die Testklasse

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

Yay! fertig.

Ok, jetzt mit dem aktuellen Weg ...

  1. Erstellen Sie eine neue Handler-Klasse - ja, eine Klasse!
  2. Fügen Sie den Handler in den Dienst ein
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");
        }
        ....
    } 
}

Aber der Schmerz ist jetzt, dass ich eine Klasse machen muss. Jetzt hast du mich verletzt.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

Und diese Klasse fängt ziemlich einfach an. Bis ich, wenn ich mehrere GetAsync calls habe (also muss ich mehrere Handler-Instanzen bereitstellen?) -oder- mehrere httpClients in einem einzigen Dienst. Verwenden wir außerdem Dispose des Handlers oder verwenden wir ihn wieder, wenn wir mehrere Aufrufe im selben Logikblock ausführen (was eine ctor-Option ist)?

z.B.

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;
    }
    ....
}

das kann so viel einfacher gemacht werden..

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");

sauber sauber sauber :)

Dann - da ist das nächste Bit: Auffindbarkeit

Als ich anfing, MS.Net.Http.HttpClient zu verwenden, war die API ziemlich offensichtlich :+1: Ok - String holen und asynchron machen ... einfach ...

... aber dann drücke ich auf Testen .... und jetzt soll ich etwas über HttpClientHandlers lernen? Ähm warum? Ich bin der Meinung, dass dies alles unter der Decke verborgen werden sollte und ich mich nicht um all diese Implementierungsdetails kümmern muss. Es ist zu viel! Du sagst, dass ich anfangen sollte, in die Kiste zu schauen und etwas über die Klempnerarbeit zu lernen ... was weh tut :cry:

Das alles macht es komplexer, als es sein sollte, IMO.


Helfen Sie mir, Microsoft – Sie sind meine einzige Hoffnung.

Ich würde auch gerne eine einfache und einfache Möglichkeit sehen, verschiedene Dinge zu testen, die HttpClient verwenden. :+1:

Danke für die ausführliche Antwort und die Codeschnipsel - das hilft wirklich.

Zunächst stelle ich fest, dass Sie neue Instanzen von HttpClient zum Senden jeder Anfrage erstellen – das ist nicht das beabsichtigte Entwurfsmuster für HttpClient. Das Erstellen einer HttpClient-Instanz und deren Wiederverwendung für alle Ihre Anforderungen trägt zur Optimierung des Verbindungspoolings und der Speicherverwaltung bei. Bitte erwägen Sie die Wiederverwendung einer einzelnen Instanz von HttpClient. Sobald Sie dies getan haben, können Sie den gefälschten Handler in nur eine Instanz von HttpClient einfügen, und Sie sind fertig.

Zweitens - Sie haben recht. Das API-Design von HttpClient eignet sich nicht für einen rein schnittstellenbasierten Mechanismus zum Testen. Wir erwägen, in Zukunft ein statisches Methoden-/Factory-Muster hinzuzufügen, mit dem Sie das Verhalten aller HttpClient-Instanzen ändern können, die von dieser Factory oder nach dieser Methode erstellt wurden. Wir haben uns dafür aber noch nicht auf ein Design festgelegt. Das Hauptproblem bleibt jedoch bestehen – Sie müssen einen gefälschten Handler definieren und ihn unter dem HttpClient-Objekt einfügen.

@ericstj - Gedanken dazu?

Danke,
Sid.

@SidarthNabar, warum zögern Sie/Ihr Team so sehr, eine Schnittstelle für diese Klasse anzubieten? Ich hatte gehofft, dass der springende Punkt, dass ein Entwickler etwas über Handler _lernen_ und dann gefälschte Handler-Klassen _erstellen_ muss, genug Schmerzpunkt ist, um die Notwendigkeit einer Schnittstelle zu rechtfertigen (oder zumindest hervorzuheben).

Ja, ich verstehe nicht, warum ein Interface eine schlechte Sache sein sollte. Es würde das Testen von HttpClient so viel einfacher machen.

Einer der Hauptgründe, warum wir Schnittstellen im Framework vermeiden, ist, dass sie nicht gut versioniert werden. Leute können immer ihre eigenen erstellen, wenn sie Bedarf haben, und das wird nicht kaputt gehen, wenn die nächste Version des Frameworks ein neues Mitglied hinzufügen muss. @KrzysztofCwalina oder @terrajobst können möglicherweise mehr über die Geschichte/Details dieser Designrichtlinie mitteilen. Dieses spezielle Thema ist eine fast religiöse Debatte: Ich bin zu pragmatisch, um mich daran zu beteiligen.

HttpClient hat viele Optionen für die Unit-Testbarkeit. Es ist nicht versiegelt und die meisten seiner Mitglieder sind virtuell. Es hat eine Basisklasse, die verwendet werden kann, um die Abstraktion zu vereinfachen, sowie einen sorgfältig entworfenen Erweiterungspunkt in HttpMessageHandler, wie @sharwell betont . IMO ist es eine ziemlich gut gestaltete API, dank @HenrikFrystykNielsen.

:+1: eine Schnittstelle

:wave: @ericstj Vielen Dank fürs Einspringen :+1: :cake:

Einer der Hauptgründe, warum wir Schnittstellen im Framework vermeiden, ist, dass sie nicht gut versioniert werden.

Ja - toller Punkt.

Dieses spezielle Thema ist eine fast religiöse Debatte

ja ... Punkt gut getroffen.

HttpClient hat viele Optionen für die Unit-Testbarkeit

Oh? Ich kämpfe damit :blush: daher der Grund für dieses Problem :blush:

Es ist nicht versiegelt und die meisten seiner Mitglieder sind virtuell.

Mitglieder sind virtuell? Oh Mist, das habe ich total übersehen! Wenn sie virtuell sind, können Mocking Frameworks diese Mitglieder verspotten :+1: und wir brauchen keine Schnittstelle!

Schauen wir uns mal HttpClient.cs an ...

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

Hmm. die sind nicht virtuell ... suchen wir die Datei ... äh ... kein virtual Schlüsselwort gefunden. Meinen Sie also, _andere_ Mitglieder von _anderen verwandten_ Klassen sind virtuell? Wenn ja ... dann sind wir wieder bei dem Thema, das ich anspreche - wir müssen jetzt unter die Haube schauen, um zu sehen, was GetAsync tut, damit wir dann wissen, was wir erstellen / verdrahten / etc ...

Ich glaube, ich verstehe etwas wirklich Grundlegendes nicht, hier ¯(°_°)/¯ ?

EDIT: Vielleicht können diese Methoden virtuell sein? Ich kann PR!

SendAsync ist virtuell und fast alle anderen API-Schichten darüber. 'Die meisten' war falsch, da täuscht mich meine Erinnerung. Mein Eindruck war, dass die meisten praktisch virtuell waren, da sie auf einem virtuellen Mitglied aufbauen. Normalerweise machen wir Dinge nicht virtuell, wenn es sich um kaskadierende Überladungen handelt. Es gibt eine spezifischere Überladung von SendAsync, die nicht virtuell ist und behoben werden könnte.

Ah! Gotcha :blush: All diese Methoden enden also mit dem Aufruf SendAsync .. was die ganze schwere Arbeit erledigt. Das bedeutet also immer noch, dass wir hier ein Auffindbarkeitsproblem haben ... aber lassen wir das beiseite (das ist rechthaberisch).

Wie würden wir uns SendAsync lustig machen und dieses grundlegende Beispiel geben ...
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);
    }
}

Einer der Hauptgründe, warum wir Schnittstellen im Framework vermeiden, ist, dass sie nicht gut versioniert werden. Leute können immer ihre eigenen erstellen, wenn sie Bedarf haben, und das wird nicht kaputt gehen, wenn die nächste Version des Frameworks ein neues Mitglied hinzufügen muss.

Ich kann diese Denkweise mit dem vollständigen .NET Framework vollkommen nachvollziehen.

.NET Core ist jedoch in viele kleine Pakete aufgeteilt, die jeweils unabhängig versioniert sind. Verwenden Sie die semantische Versionierung, erhöhen Sie die Hauptversion, wenn es eine Breaking Change gibt, und Sie sind fertig. Die einzigen betroffenen Personen werden diejenigen sein, die ihre Pakete ausdrücklich auf die neue Hauptversion aktualisieren – in dem Wissen, dass es dokumentierte Breaking Changes gibt.

Ich befürworte nicht, dass Sie jeden Tag jede Schnittstelle kaputt machen sollten: Breaking Changes sollten idealerweise in einer neuen Hauptversion zusammengefasst werden.

Ich finde es traurig, APIs aus Gründen der zukünftigen Kompatibilität lahmzulegen. War es nicht eines der Ziele von .NET Core, schneller zu iterieren, da Sie sich keine Sorgen mehr über ein subtiles .NET-Update machen müssen, das Tausende bereits installierter Anwendungen beschädigt?

Meine zwei Cent.

@MrJul :+1:
Versionierung sollte kein Problem sein. Deshalb gibt es die Versionsnummern :)

Die Versionierung ist immer noch ein großes Problem. Das Hinzufügen eines Members zu einer Schnittstelle ist eine Breaking Change. Für Kernbibliotheken wie diese, die sich im Desktop-Posteingang befinden, möchten wir in zukünftigen Versionen Funktionen auf den Desktop zurückbringen. Wenn wir forken, bedeutet das, dass die Leute keinen portablen Code schreiben können, der an beiden Orten läuft. Weitere Informationen zu Breaking Changes finden Sie unter: https://github.com/dotnet/corefx/wiki/Breaking-Changes.

Ich denke, das ist eine gute Diskussion, aber wie ich bereits erwähnt habe, habe ich nicht viel Kratzer in der Auseinandersetzung um Schnittstellen vs. abstrakte Klassen. Vielleicht ist dies ein Thema für das nächste Design-Meeting?

Ich bin mit der von Ihnen verwendeten Testbibliothek nicht sehr vertraut, aber ich würde einen Hook bereitstellen, mit dem Tests die Instanz des Clients festlegen und dann ein Scheinobjekt erstellen können, das sich so verhält, wie ich es möchte. Das Scheinobjekt könnte änderbar sein, um eine gewisse Wiederverwendung zu ermöglichen.

Wenn Sie HttpClient eine bestimmte kompatible Änderung vorschlagen möchten, die die Testbarkeit der Einheiten verbessert, schlagen Sie sie bitte vor und @SidarthNabar und sein Team können sie berücksichtigen.

Wenn die API nicht verspottbar ist, sollten wir sie beheben. Aber es hat nichts mit Schnittstellen oder Klassen zu tun. Interface ist aus Sicht der Mockability für alle praktischen Zwecke nicht anders als eine reine abstrakte Klasse.

@KrzysztofCwalina Sie, mein Herr, haben den Nagel auf den Kopf getroffen! perfekte Zusammenfassung!

Ich schätze, ich nehme die weniger beliebte Seite dieses Arguments, da ich persönlich nicht glaube, dass eine Schnittstelle notwendig ist. Wie bereits erwähnt, ist HttpClient bereits die Abstraktion. Das Verhalten kommt wirklich von HttpMessageHandler . Ja, es ist nicht mockable/fakeable mit den Ansätzen, an die wir uns mit Frameworks wie Moq oder FakeItEasy gewöhnt haben, aber es muss nicht sein; das ist nicht die _einzige_ Art, Dinge zu tun. Die API ist jedoch immer noch perfekt testbar.

Lassen Sie uns also die Frage „Ich muss meine eigenen HttpMessageHandler erstellen?“ ansprechen. Nein natürlich nicht. Wir haben nicht alle unsere eigenen spöttischen Bibliotheken geschrieben. @PureKrome hat bereits seine HttpClient.Helpers -Bibliothek gezeigt. Ich persönlich habe das noch nicht benutzt, aber ich werde es mir ansehen. Ich habe die MockHttp -Bibliothek von @richardszalay verwendet, die ich fantastisch finde. Wenn Sie jemals mit $httpBackend von AngularJS gearbeitet haben, verfolgt MockHttp genau den gleichen Ansatz.

In Bezug auf Abhängigkeiten sollte Ihre Serviceklasse das Einfügen HttpClient zulassen und offensichtlich den angemessenen Standardwert von new HttpClient() liefern können. Wenn Sie in der Lage sein müssen, Instanzen zu erstellen, nehmen Sie ein Func<HttpClient> oder, wenn Sie aus irgendeinem Grund kein Fan von Func<> sind, entwerfen Sie Ihre eigene IHttpClientFactory Abstraktion und a Die Standardimplementierung davon würde wiederum nur new HttpClient() zurückgeben.

Abschließend finde ich diese API in ihrer jetzigen Form perfekt testbar und sehe keine Notwendigkeit für eine Schnittstelle. Ja, es erfordert einen anderen Ansatz, aber die oben genannten Bibliotheken sind bereits vorhanden, um uns bei diesem anderen Teststil auf vollkommen logische, intuitive Weise zu helfen. Wenn wir mehr Funktionalität/Einfachheit wollen, lassen Sie uns zu diesen Bibliotheken beitragen, um sie besser zu machen.

Mir persönlich sind ein Interface oder virtuelle Methoden nicht so wichtig. Aber komm schon, perfectly testable ist ein bisschen zu viel :)

@luisrudge Nun, können Sie ein Szenario angeben, das nicht mit dem Message-Handler-Teststil getestet werden kann, der so etwas wie MockHttp ermöglicht? Vielleicht würde das helfen, den Fall zu klären.

Ich bin auf nichts gestoßen, was ich noch nicht kodifizieren könnte, aber vielleicht vermisse ich einige esoterischere Szenarien, die nicht abgedeckt werden können. Selbst dann könnte es sich nur um ein fehlendes Feature dieser Bibliothek handeln, das jemand beitragen könnte.

Im Moment bleibe ich bei der Meinung, dass es als Abhängigkeit "perfekt testbar" ist, nur nicht so, wie .NET-Entwickler es gewohnt sind.

Es ist testbar, da stimme ich zu. aber es tut trotzdem weh. Wenn Sie eine externe Bibliothek verwenden müssen, die Ihnen dabei hilft, ist sie nicht perfekt testbar. Aber ich denke, das ist zu subjektiv, um darüber zu diskutieren.

Man könnte argumentieren, dass aspnet mvc 5 perfekt testbar ist, Sie müssen nur 50LOC schreiben, um alles zu verspotten, was ein Controller benötigt. IMHO, das ist der gleiche Fall mit HttpClient. Es ist testbar? Jawohl. Es ist einfach zu testen? Nein.

@luisrudge Ja, ich stimme zu, es ist subjektiv und ich verstehe den Wunsch nach Schnittstellen/Virtuals vollkommen. Ich versuche nur sicherzustellen, dass jeder, der vorbeikommt und diesen Thread liest, zumindest etwas über die Tatsache erfährt, dass diese API in einer Codebasis auf sehr testbare Weise _genutzt_ werden kann, ohne all Ihre eigenen Abstraktionen um HttpClient herum einzuführen.

Wenn Sie eine externe Bibliothek verwenden müssen, die Ihnen dabei hilft, ist sie nicht perfekt testbar

Nun, wir alle verwenden bereits die eine oder andere Bibliothek zum Spotten/Fälschen * und es ist wahr, wir können _diese_ nicht einfach zum Testen dieses API-Stils verwenden, aber ich denke nicht, dass es weniger testbar ist als ein schnittstellenbasierter Ansatz, nur weil ich eine andere Bibliothek einbringen muss.

_ * Zumindest hoffe ich nicht!_

@drub0y von meinem OP Ich habe gesagt, dass die Bibliothek testbar ist - aber es ist nur ein echter Schmerz im Vergleich (zu dem, was ich leidenschaftlich glaube) zu dem, was es sein _könnte_. IMO @luisrudge hat es perfekt formuliert:

Man könnte argumentieren, dass aspnet mvc 5 perfekt testbar ist, Sie müssen nur 50LOC schreiben, um alles zu verspotten, was ein Controller benötigt

Dieses Repo ist ein wichtiger Bestandteil einer _großen_ Anzahl von Entwicklern. Die standardmäßige (und verständliche) Taktik ist also, mit diesem Repo _sehr_ vorsichtig zu sein. Klar - das verstehe ich total.

Ich würde gerne glauben, dass dieses Repo noch optimiert werden kann (in Bezug auf die API), bevor es RTW geht. Zukünftige Veränderungen werden plötzlich _wirklich_ schwierig, einschließlich der _Wahrnehmung_, sich zu ändern.

Also mit der aktuellen API - kann es getestet werden? Ja. Besteht es den Dark Matter Developer Test? Ich persönlich glaube nicht.

Der Lackmustest IMO ist folgender: Kann ein normaler Joe eines der gängigen/beliebten Testframeworks + Mocking Frameworks aufgreifen und eine der Hauptmethoden in dieser API verspotten? Jetzt gerade – nein. Dann muss der Entwickler also aufhören, was er tut, und anfangen, sich über die _Implementierung_ dieser Bibliothek zu informieren.

Wie bereits erwähnt, ist HttpClient bereits die Abstraktion. Das Verhalten kommt wirklich vom HttpMessageHandler.

Das ist mein Punkt - warum bringen Sie uns dazu, Zyklen damit zu verbringen, dies herauszufinden, wenn der Zweck der Bibliothek darin besteht, all diese Magie zu abstrahieren ... nur um zu sagen: "Ok ... also haben wir etwas gezaubert, aber jetzt wollen Sie sich darüber lustig machen unsere Magie ... Es tut mir leid, Sie müssen jetzt Ihre Ärmel hochziehen, das Dach der Bibliothek hochheben und anfangen, darin zu graben usw. "

Es fühlt sich so ... verworren an.

Wir sind in der Lage, so vielen Menschen das Leben leicht zu machen und immer wieder in die Defensive zu kommen: „Es geht – aber … lesen Sie die FAQ/Kodex“.

Hier ist ein weiterer Blickwinkel, um dieses Problem anzugehen: Geben Sie zufälligen Nicht-MS-Entwicklern zwei Beispiele ... joe Citizens-Entwickler, die wissen, wie man xUnit und Moq/FakeItEasy/InsertTheHipMockingFrameworkThisYear verwendet.

  • Erste API, die eine Schnittstelle/virtuell/was auch immer verwendet ... aber die Methode kann verspottet werden.
  • Zweite API, die nur eine öffentliche Methode ist, und um den _Integrationspunkt_ zu verspotten, müssen sie den Code lesen.

Darauf läuft es hinaus, IMO.

Finden Sie heraus, welcher Entwickler dieses Problem lösen kann _und_ bleiben Sie zufrieden.

Wenn die API nicht verspottbar ist, sollten wir sie beheben.

Im Moment ist es meiner Meinung nach nicht - aber es gibt Möglichkeiten, dies erfolgreich zu umgehen (auch das ist eigensinnig - ich gebe das zu)

Aber es hat nichts mit Schnittstellen oder Klassen zu tun. Interface ist aus Sicht der Mockability für alle praktischen Zwecke nicht anders als eine reine abstrakte Klasse.

Genau - das ist ein Ausführungsdetail. Ziel ist es, ein kampferprobtes Mock-Framework nutzen zu können und los geht's.

@luisrudge Wenn Sie eine externe Bibliothek verwenden müssen, die Ihnen dabei hilft, ist sie nicht perfekt testbar
@drub0y Nun, wir alle verwenden bereits die eine oder andere Bibliothek zum Spotten / Fälschen

(Ich hoffe, ich habe das letzte Zitat / den letzten Absatz verstanden.) Nicht ... ruhig. Was @luisrudge gesagt hat, ist: „Wir haben ein Tool zum Testen. Ein zweites Tool zum Spotten. Bisher sind dies generische/allgemeine Tools, die an nichts gebunden sind. Aber … Sie möchten jetzt, dass ich ein drittes Tool herunterlade, das _spezifisch_ ist eine _spezifische_ API/Dienst in unserem Code zu verspotten, weil diese spezifische API/Dienst, die wir verwenden, nicht dafür entworfen wurde, gut getestet zu werden?". Das ist ein bisschen reich :(

In Bezug auf Abhängigkeiten sollte Ihre Dienstklasse das Einfügen eines HttpClient zulassen und offensichtlich den angemessenen Standard von new HttpClient() bereitstellen können.

Völlig einverstanden! Können Sie also den obigen Beispielcode umgestalten, um uns zu zeigen, wie einfach dies sein sollte/kann? Es gibt viele Möglichkeiten, eine Katze zu häuten ... also was ist ein einfacher UND einfacher Weg? Zeig uns den Code.

Ich fange an. Ich entschuldige mich bei @KrzysztofCwalina für die Verwendung einer Schnittstelle, aber dies ist nur ein Kickstart für meinen Lackmustest.

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

Es hört sich so an, als ob @PureKrome nur ein Dokumentationsupdate benötigt, das erklärt, welche Methoden simuliert/überschrieben werden müssen, um das Verhalten der API während des Testens anzupassen.

@sharwell das ist absolut nicht der Fall. Wenn ich Sachen teste, möchte ich nicht den gesamten httpclient-Code ausführen:
Sehen Sie sich die SendAsync-Methode an .
Das ist verrückt. Was Sie vorschlagen, ist, dass jeder Entwickler die Interna aus der Klasse kennen, Github öffnen, den Code und solche Sachen sehen sollte, um es testen zu können. Ich möchte, dass dieser Entwickler die GetStringAsync verspottet und Scheiße erledigt.

@luisrudge Eigentlich wäre mein Vorschlag, diese Bibliothek überhaupt nicht zu verspotten. Da Sie bereits eine saubere Abstraktionsschicht mit einer entkoppelten, austauschbaren Implementierung haben, ist zusätzliches Mocking unnötig. Das Verspotten dieser Bibliothek hat die folgenden zusätzlichen Nachteile:

  1. Erhöhte Belastung für Entwickler, die Tests erstellen (Pflege der Mocks), was wiederum die Entwicklungskosten erhöht
  2. Erhöhte Wahrscheinlichkeit, dass Ihre Tests bestimmte problematische Verhaltensweisen nicht erkennen, wie z. B. die Wiederverwendung des mit einer Nachricht verknüpften Inhaltsstreams (das Senden einer Nachricht führt zu einem Aufruf an Dispose im Inhaltsstream, aber die meisten Mocks ignorieren dies)
  3. Unnötig engere Kopplung der Anwendung an eine bestimmte Abstraktionsschicht, was die Entwicklungskosten im Zusammenhang mit der Bewertung von Alternativen zu HttpClient erhöht

Mocking ist eine Teststrategie, die auf verflochtene Codebasen abzielt, die in kleinem Maßstab schwer zu testen sind. Während die Verwendung von Mocking mit erhöhten Entwicklungskosten korreliert, korreliert der _Bedarf_ für Mocking mit einer erhöhten Menge an Kopplung im Code. Das bedeutet, dass das Verspotten selbst ein Code-Geruch ist. Wenn Sie die gleiche Input-Output-Testabdeckung über und innerhalb Ihrer API bereitstellen können, ohne Mocking zu verwenden, werden Sie im Grunde in jedem Entwicklungsaspekt profitieren.

Da Sie bereits eine saubere Abstraktionsschicht mit einer entkoppelten, austauschbaren Implementierung haben, ist zusätzliches Mocking unnötig

Bei allem Respekt, wenn die Option darin besteht, einen eigenen Faked Message Handler zu schreiben, was ohne Durchforsten des Codes nicht offensichtlich ist, dann ist das eine sehr hohe Hürde, die für viele Entwickler sehr frustrierend sein wird.

Ich stimme dem Kommentar von @luisrudge vorhin zu. _Technisch_ MVC5 war testbar, aber mein Gott, war es eine Nervensäge und eine immense Quelle von Haarrissen.

@sharwell Wollen Sie damit sagen, dass das Verspotten IHttpClient eine Belastung ist und das Implementieren eines gefälschten Nachrichtenhandlers (wie dieser nicht? Dem kann ich nicht zustimmen, sorry.

@luisrudge Für einfache Fälle des Testens von GetStringAsync ist es nicht annähernd so schwer, den Handler zu verspotten. Wenn Sie den sofort einsatzbereiten Moq + HttpClient verwenden, benötigen Sie lediglich Folgendes:

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

Wenn das zu viel ist, können Sie eine Hilfsklasse definieren:

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

Wenn Sie die Hilfsklasse verwenden, benötigen Sie Folgendes:

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

Also zum Vergleich der beiden Versionen:
_Disclaimer_: Dies ist ein ungetesteter Browsercode. Außerdem habe ich Moq seit Ewigkeiten nicht mehr benutzt.

Aktuell

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

im Gegensatz dazu, eine Möglichkeit anzubieten, die API-Methode direkt zu verspotten.

Ein anderer Weg

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

Wenn man das herunterdestilliert, kommt es hauptsächlich darauf an (mit Ausnahme des geringfügigen Unterschieds in 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);

dagegen ...

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

Sieht der Code _grob_ für beide ungefähr richtig aus? (beide müssen ziemlich genau sein, bevor wir die beiden vergleichen können).

@sharwell , also muss ich meinen Code über den gesamten HttpClient.cs-Code ausführen, um meine Tests auszuführen? Wie ist das weniger mit HttpClient gekoppelt?

Wir verwenden HttpClient stark in einigen unserer Systeme und ich würde es nicht als perfekt testbares Stück Technik bezeichnen. Ich mag die Bibliothek wirklich, aber eine Art Interface oder eine andere Verbesserung zum Testen wäre meiner Meinung nach eine große Verbesserung.

Hut ab vor @PureKrome für seine Bibliothek, aber es wäre besser, wenn es nicht nötig wäre :)

Ich habe gerade alle Kommentare gelesen und viel gelernt, aber ich habe eine Frage: Warum müssen Sie den HttpClient in Ihren Tests verspotten?

In den meisten gängigen Szenarien wird die von HttpClient bereitgestellte Funktionalität bereits in eine Klasse wie HtmlDownloader.DownloadFile() . Warum verspotten Sie nicht einfach diese Klasse statt ihrer Klempnerarbeit?

Ein weiteres mögliches Szenario, das ein solches Spotten erfordern würde, ist, wenn Sie eine heruntergeladene Zeichenfolge analysieren müssen und dieses Stück Code auf mehrere unterschiedliche Ausgaben getestet werden muss. Selbst in diesem Fall könnte diese Funktionalität (Parsing) in eine Klasse/Methode extrahiert und ohne Mocks getestet werden.

Ich sage nicht, dass die Testbarkeit nicht verbessert werden kann, und impliziere auch nicht, dass dies nicht der Fall sein sollte. Ich möchte nur verstehen, um etwas Neues zu lernen.

@tucaz Stellen wir uns vor, ich habe eine Website, die einige Informationen über Sie zurückgibt. Ihr Alter, Name, Geburtstag usw. Sie haben zufällig auch Aktien von einigen Unternehmen.

Sie haben gesagt, Sie besitzen 100 Einheiten AAA-Aktien und 200 Einheiten BBB-Aktien? Wenn wir also Ihre Kontodetails anzeigen, _sollten_ wir anzeigen, wie viel Geld Ihre Aktien wert sind.

Dazu müssen wir die aktuellen Aktienkurse von _einem anderen_ System abrufen. Dazu müssen wir also die Bestände mit einem Service abrufen, oder? Kommen wir dazu...

_Haftungsausschluss: Browsercode..._

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

Ok, hier haben wir also einen einfachen Dienst, der sagt: "_Los, hol mir die Aktienkurse für die Aktien AAA und BBB_";

Also das muss ich mal testen:

  • Angesichts einiger legitimer Aktiennamen
  • Angesichts einiger ungültiger/nicht vorhandener Aktiennamen
  • httpClient löst eine Ausnahme aus (das Internet ist beim Warten auf eine Antwort explodiert)

und jetzt kann ich diese Szenarien leicht testen. Ich bemerke, dass, wenn der httpClient explodiert (eine Ausnahme auslöst) .. dieser Dienst explodiert .. also muss ich vielleicht etwas Protokollierung hinzufügen und null zurückgeben? donno .. aber zumindest kann ich über das problem nachdenken und es dann angehen.

Natürlich - ich möchte zu 100% nicht wirklich den echten Webdienst während meiner normalen Komponententests treffen. Indem ich den Schein spritze, kann ich das verwenden. Andernfalls kann ich eine neue Instanz erstellen und diese verwenden.

Also versuche ich festzustellen, dass mein Vorschlag viel einfacher (zu lesen / zu pflegen / usw.) ist als der aktuelle Status quo.

In den meisten gängigen Szenarien ist die von HttpClient bereitgestellte Funktionalität bereits in eine Klasse wie HtmlDownloader.DownloadFile() eingeschlossen.

Warum sollten wir HttpClient umschließen? Es ist _bereits_ eine Abstraktion über all den http :sparkles: darunter. Es ist eine großartige Bibliothek! Ich persönlich sehe nicht, was das bringen würde?

@PureKrome Ihr Verwendungsbeispiel ist genau das, was ich meine, wenn ich die Funktionalität in eine Wrapping-Klasse verpacke.

Ich bin mir nicht sicher, ob ich verstehen kann, was Sie zu testen versuchen, aber für mich sieht es so aus, als würden Sie den zugrunde liegenden Webservice testen, wenn Sie wirklich behaupten müssen, dass derjenige, der ihn konsumiert, in der Lage sein wird, alles zu verarbeiten, was von GetStockAsync zurückkommt

Bitte beachten Sie Folgendes:

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

Ich schlage vor, dass Sie Ihre Abstraktion eine Ebene höher setzen und sich mit der eigentlichen Geschäftslogik befassen, da es nichts gibt, was bezüglich des Verhaltens der Methode GetStockAsync getestet werden muss.

Das einzige Szenario, das ich mir vorstellen kann, um HttpClient zu verspotten, ist, wenn Sie eine Wiederholungslogik testen müssen, abhängig von der vom Server empfangenen Antwort oder so etwas. In diesem Fall erstellen Sie ein komplizierteres Stück Code, das wahrscheinlich in eine HttpClientWithRetryLogic -Klasse extrahiert und nicht in jeder Methode verbreitet wird, die Sie codieren, die die HttpClient -Klasse verwendet.

Wenn dies der Fall ist, können Sie den Nachrichtenhandler wie vorgeschlagen verwenden oder eine Klasse erstellen, um die gesamte Wiederholungslogik zu umschließen, da HttpClient nur ein wirklich kleiner Teil der wichtigen Sache ist, die Ihre Klasse tut.

@tucaz Haben Sie nicht gerade getestet, dass Ihre Fälschung das zurückgibt, was Sie konfiguriert haben, und die spöttische Bibliothek effektiv getestet? @PureKrome möchte die konkrete Implementierung StockService testen.

Die neu eingeführte Abstraktionsebene sollte HttpClient Aufrufe umschließen, um StockService richtig testen zu können. Wahrscheinlich delegiert der Wrapper einfach jeden Aufruf an HttpClient und fügt der Anwendung unnötige Komplexität hinzu.

Sie können jederzeit eine neue Abstraktionsebene hinzufügen, um nicht mockbare Typen auszublenden. Was in diesem Thread kritisiert wird, ist, dass das Spotten HttpClient möglich ist, aber wahrscheinlich einfacher / im Einklang mit dem sein sollte, was normalerweise mit anderen Bibliotheken gemacht wird.

@MrJul Sie haben Recht. Deshalb habe ich gesagt, dass das, was durch sein Beispiel getestet wurde, die Webservice-Implementierung selbst ist, und warum ich vorgeschlagen habe (ohne Codebeispiele. Mein Fehler), dass er IStockService verspottet, damit er behaupten kann, dass der Code, der es verbraucht, dazu in der Lage ist verarbeiten, was auch immer GetStockAsync zurückgibt.

Das sollte meiner Meinung nach erstmal getestet werden. Nicht das spöttische Framework (wie ich es getan habe) oder die Webservice-Implementierung (wie @PureKrome es tun möchte).

Am Ende des Tages kann ich verstehen, dass es schön wäre, eine IHttpClient -Schnittstelle zu haben, sehe aber in solchen Szenarien keinen Nutzen, da meiner Meinung nach getestet werden muss, wie die Benutzer der Dienstabstraktion arbeiten (und damit der Webservice) kann seine Rückgabe entsprechend behandeln.

@tucaz nein. Dein Test ist falsch. Sie testen die spöttische Bibliothek. Am Beispiel von @PureKrome möchte man drei Dinge testen:

  • wenn die HTTP-Anfrage das Recht METHOD verwendet
  • wenn die HTTP-Anfrage das Recht URL hat
  • ob der Antworttext erfolgreich mit einem JSON-Deserialisierer deserialisiert werden kann

Wenn Sie beispielsweise nur IStockService in Ihrem Controller verspotten, testen Sie keines der drei oben genannten Dinge.

@luisrudge stimme allen deinen Punkten zu. Aber ... ist es der Wert, all diese Dinge zu testen, die Sie aufzählen? Ich meine, wenn ich so engagiert all diese grundlegenden Dinge teste, die von mehreren Frameworks (json.net, httpclient usw.) abgedeckt werden, denke ich, dass ich damit umgehen kann, wie es heute möglich ist.

Wenn ich jedoch sicherstellen möchte, dass ich eine seltsame Antwort deserialisieren kann, kann ich nicht einfach einen Test machen, der diese Aspekte des Codes isoliert, ohne dass HttpClient in den Test eingefügt werden muss? Außerdem gibt es keine Möglichkeit zu verhindern, dass der Server die ganze Zeit die richtige Antwort sendet, die Sie in Ihren Tests erwarten. Was ist, wenn eine Antwort nicht abgedeckt ist? Ihre Tests werden es nicht bekommen und Sie werden sowieso Probleme haben.

Meiner Meinung nach haben solche Tests, die Sie vorschlagen, einen minimalen Nettowert und Sie sollten nur daran arbeiten, wenn Sie die anderen 99 % Ihrer Anwendung abgedeckt haben. Aber am Ende ist alles eine Frage der persönlichen Vorlieben und dies ist ein subjektives Thema, bei dem verschiedene Menschen unterschiedliche Werte sehen. Und das bedeutet natürlich nicht, dass HttpClient nicht besser testbar sein sollte.

Ich denke, dass das Design dieses speziellen Codestücks all diese Punkte berücksichtigt hat, und selbst wenn es nicht das optimale Ergebnis ist, glaube ich nicht, dass die Kosten für eine Änderung jetzt geringer wären als die möglichen Vorteile.

Das Ziel, das ich bei meinem ersten Gespräch im Sinn hatte, war, @PureKrome dabei zu helfen, seine Bemühungen nicht auf das zu konzentrieren, was meiner Meinung nach eine Aufgabe mit geringem Wert ist. Aber auch hier kommt es irgendwann auf die persönliche Meinung an.

Sicher zu sein, dass Sie die richtige URL treffen, hat für Sie einen geringen Nettowert? Wie wäre es, wenn Sie sicherstellen, dass Sie eine PUT-Anfrage stellen, um eine Ressource anstelle eines Beitrags zu bearbeiten? Die anderen 99 % meiner App können abgedeckt werden, wenn dieser Anruf fehlschlägt, wird nichts funktionieren.

Soooo, ich denke wirklich, dass das Reden über HttpClient::GetStringAsync ein schlechter Weg ist, um zu argumentieren, dass man überhaupt in der Lage ist, HttpClient zu verspotten. Wenn Sie eine der HttpClient::Get[String|Stream|ByteArray]Async -Methoden verwenden, haben Sie bereits verloren, weil Sie keine ordnungsgemäße Fehlerbehandlung durchführen können, da alles hinter einem HttpRequestException versteckt ist, das geworfen würde und das nicht tut nicht einmal wichtige Details wie den HTTP-Statuscode preisgeben. Das bedeutet also, dass jede Abstraktion, die Sie auf HttpClient aufgebaut haben, nicht in der Lage ist, auf bestimmte HTTP-Fehler zu reagieren und sie in domänenspezifische Ausnahmen zu übersetzen, und jetzt haben Sie eine undichte Abstraktion ... und eine das gibt den Aufrufern Ihres Domain-Dienstes nicht weniger schreckliche Ausnahmeinformationen.

Ich denke, es ist wichtig, darauf hinzuweisen, denn in den meisten Fällen bedeutet dies, dass Sie bei einem spöttischen Ansatz die HttpClient -Methoden verspotten würden, die $ HttpResponseMessage zurückgeben, was Sie bereits zum bringt dieselbe Menge an Arbeit, die erforderlich ist, um HttpMessageHandler::SendAsync zu verspotten/vorzutäuschen. Tatsächlich ist es mehr! Sie müssten möglicherweise mehrere Methoden (z. B. [Get|Put|Delete|Send]Async ) auf der Ebene HttpClient verspotten, anstatt einfach SendAsync auf der Ebene HttpMessageHandler zu verspotten.

@luisrudge erwähnte auch die Deserialisierung von JSON, also sprechen wir jetzt über die Arbeit mit HttpContent , was bedeutet, dass Sie mit einer der Methoden arbeiten, die mit Sicherheit HttpResponseMessage zurückgeben. Es sei denn, Sie sagen, dass Sie die gesamte Architektur des Medienformatierers umgehen und Zeichenfolgen von GetStringAsync manuell mit JsonSerializer::Deserialize oder was auch immer in Objektinstanzen deserialisieren. Wenn das der Fall ist, dann können wir, glaube ich, nicht mehr wirklich über das _Testen_ der API reden, denn das würde bedeuten, die API _an sich_ völlig falsch zu verwenden. :enttäuscht:

@luisrudge es ist in der Tat wichtig, aber Sie müssen HttpClient nicht berühren, um sicherzustellen, dass es richtig ist. Stellen Sie einfach sicher, dass die Quelle, aus der Sie die URL gelesen haben, die Dinge korrekt zurückgibt, und Sie können loslegen. Auch hier müssen Sie keinen Integrationstest mit HttpClient durchführen.

Dies sind wichtige Dinge, aber sobald Sie sie richtig gemacht haben, ist die Wahrscheinlichkeit, dass sie es vermasseln, sehr, sehr gering, es sei denn, Sie haben woanders ein undichtes Rohr.

@tucaz stimme zu. Trotzdem: Es ist das Wichtigste und ich würde es gerne testen lassen :)

@drub0y Ich bin erstaunt, dass die Verwendung einer öffentlichen Methode falsch ist :)

@luisrudge Nun, eine andere Diskussion, nehme ich an, aber ich glaube nicht, dass sie für eine Schnittstelle plädiert, die verspottet werden kann, indem die inhärenten Mängel bei der Verwendung dieser "Bequemlichkeits" -Methoden in allen außer den meisten Abhilfemaßnahmen ignoriert werden von Codierungsszenarien (z. B. Utility-Apps und Stage-Demos). Wenn Sie sich dafür entscheiden, sie im Produktionscode zu verwenden, ist das Ihre Wahl, aber Sie tun dies, während Sie hoffentlich ihre Mängel anerkennen.

Wie auch immer, kehren wir zu dem zurück, was @PureKrome sehen wollte, nämlich wie man seine Klassen so entwerfen sollte, dass sie zum HttpClient API-Design passen, damit sie ihren Code einfach testen können.

Hier ist zunächst ein Beispiel-Domaindienst, der HTTP-Aufrufe tätigt, um die Anforderungen seiner Domain zu erfüllen:

``` c#
öffentliche Schnittstelle IsomeDomainService
{
AufgabeGetSomeThingsValueAsync(string id);
}

öffentliche Klasse SomeDomainService : IsomeDomainService
{
privater schreibgeschützter 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");
}

Ähm, das ist ziemlich einfach und geradlinig; liest sich klar wie der Tag IMNSHO. Beachten Sie auch, dass ich FluentAssertions verwende, was wiederum eine weitere Bibliothek ist, von der ich denke, dass viele von uns sie verwenden, und ein weiterer Schlag gegen das Argument „Ich möchte keine weitere Bibliothek einführen müssen“.

Lassen Sie mich nun auch veranschaulichen, warum Sie GetStringAsync wahrscheinlich niemals _wirklich_ für irgendeinen Produktionscode verwenden möchten. Sehen Sie sich die Methode SomeDomainService::GetSomeThingsValueAsync noch einmal an und gehen Sie davon aus, dass sie eine REST-API aufruft, die tatsächlich aussagekräftige HTTP-Statuscodes zurückgibt. Beispielsweise existiert möglicherweise ein „Ding“ mit der ID „123“ nicht, sodass die API einen 404-Status zurückgeben würde. Also möchte ich das erkennen und als folgende domänenspezifische Ausnahme modellieren:

``` c#
// Serialisierungsunterstützung der Kürze halber ausgeschlossen
öffentliche Klasse SomeThingDoesntExistException : Ausnahme
{
öffentlich SomeThingDoesntExistException (String-ID)
{
ID = ID;
}

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

Das ist es also, wovon ich gesprochen habe, als ich sagte, dass diese einfachen "bequemen" Methoden wie GetStringAsync nicht wirklich "gut" für Code auf Produktionsebene sind. Alles, was Sie bekommen können, ist HttpRequestException und von dort aus können Sie nicht sagen, welchen Statuscode Sie erhalten haben, und können ihn daher möglicherweise nicht in die richtige Ausnahme innerhalb einer Domäne übersetzen. Stattdessen müssten Sie GetAsync verwenden und HttpResponseMessage folgendermaßen interpretieren:

``` c#
öffentliche asynchrone AufgabeGetSomeThingsValueAsync (Zeichenfolge-ID)
{
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, es ist noch weniger Code!!$!% Warum? Denn ich musste nicht einmal eine Erwartung mit MockHttp einrichten und es hat mir automatisch einen 404 für die angeforderte URL als Standardverhalten zurückgegeben.

Magic is Real

@PureKrome erwähnte auch einen Fall, in dem er neue HttpClient -Instanzen innerhalb einer Dienstklasse instanziieren möchte. Wie ich bereits sagte, würden Sie in diesem Fall stattdessen Func<HttpClient> als Abhängigkeit nehmen, um sich von der eigentlichen Erstellung zu abstrahieren, oder Sie könnten Ihre eigene Factory-Schnittstelle einführen, wenn Sie kein Fan von Func sind

Ich habe meine Antwort gelöscht - ich möchte ein wenig über die Antwort und den Code von @drub0y nachdenken.

@drub0y du hast vergessen zu erwähnen, dass jemand herausfinden muss, dass man überhaupt ein MockHttpMessageHandler implementieren muss. Das ist der springende Punkt dieser Diskussion. Es ist nicht geradlinig genug. Mit aspnet5 können Sie alles testen, indem Sie einfach öffentliche Klassen von Schnittstellen oder Basisklassen mit virtuellen Methoden verspotten, aber dieser spezifische API-Eintrag ist anders, weniger auffindbar und weniger intuitiv.

@luisrudge Sicher, dem kann ich zustimmen, aber wie haben wir jemals herausgefunden, dass wir ein Moq oder FakeItEasy brauchen? Wie haben wir herausgefunden, dass wir eine N/XUnit, FluentAssertions usw. brauchen? Das sehe ich eigentlich nicht anders.

Es gibt großartige (kluge) Leute da draußen in der Community, die die Notwendigkeit für dieses Zeug erkennen und sich tatsächlich die Zeit nehmen, Bibliotheken wie diese zu starten, um diese Probleme zu lösen. (Ein großes Lob an alle!) Dann werden die Lösungen, die tatsächlich „gut“ sind, von anderen bemerkt, die die gleichen Bedürfnisse erkennen und durch OSS und Evangelisierung durch die anderen Führungskräfte in der Gemeinschaft organisch wachsen. Ich denke, die Dinge sind in dieser Hinsicht besser denn je, da die Leistungsfähigkeit von OSS endlich in der .NET-Community verwirklicht wird.

Nun, das gesagt, ich persönlich glaube, dass Microsoft etwas _wie_ MockHttp zusammen mit der Bereitstellung der System.Net.Http APIs direkt "out of the box" hätte bereitstellen sollen. Das AngularJS-Team stellte $httpBackEnd zusammen mit $http zur Verfügung, weil es erkannte, dass Menschen diese Fähigkeit unbedingt _brauchen_ würden, um ihre Apps effektiv testen zu können. Ich denke, die Tatsache, dass Microsoft dieses Problem _nicht_ identifiziert hat, ist der Grund, warum es so viel „Verwirrung“ um Best Practices mit dieser API gibt, und ich denke, warum wir jetzt alle hier darüber diskutieren. :enttäuscht: Trotzdem bin ich froh, dass @richardszalay und @PureKrome versucht haben, diese Lücke mit ihren Bibliotheken zu füllen, und ich denke, wir sollten uns jetzt hinter sie stellen, um ihre Bibliotheken so gut wie möglich zu verbessern unseres Lebens einfacher. :Lächeln:

Ich wurde gerade über diesen Thread benachrichtigt, also dachte ich, ich werfe meine 2c hinein.

Ich benutze dynamische Mocking-Frameworks genauso oft wie der nächste Typ, aber in den letzten Jahren habe ich gelernt, dass einige Domains (wie HTTP) besser bedient werden, wenn sie hinter etwas mit mehr Domain-Wissen vorgetäuscht werden. Alles, was über den einfachen URL-Abgleich hinausgeht, wäre mit dynamischem Mocking ziemlich schmerzhaft, und wenn Sie darüber iterieren, würden Sie sowieso mit Ihrer eigenen Mini-Version dessen enden, was MockHttp tut.

Nehmen Sie Rx als weiteres Beispiel: IObservable ist eine Schnittstelle und daher mockbar, aber es wäre Wahnsinn, komplexe Kompositionen (ganz zu schweigen von Schedulern) allein mit dynamischem Mocking zu testen. Die Rx-Testbibliothek gibt der Domäne, in der sie arbeitet, viel mehr semantische Bedeutung.

Zurück speziell zu HTTP, wenn auch in der JS-Welt, können Sie sinon auch verwenden, um XMLHttpRequest zu verspotten, anstatt die Test-APIs von Angular zu verwenden, aber ich wäre ziemlich überrascht, wenn es jemand tun würde.

Abgesehen davon stimme ich der Auffindbarkeit vollkommen zu. Die HTTP-Testdokumentation von Angular ist genau dort mit der HTTP-Dokumentation, aber Sie müssen über MockHttp Bescheid wissen, um danach zu suchen. Daher wäre es für mich völlig in Ordnung, MockHttp wieder in die Hauptbibliothek einzubringen, wenn jemand aus dem CoreFx-Team Kontakt aufnehmen möchte (die Lizenzen sind bereits dieselben).

Ich mag es, mich darüber lustig zu machen, aber wir verwenden auch eine Func -Signatur:

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

Dadurch kann der Code der Testklasse so einfach sein:

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

Und der Controller kann einfach Folgendes tun:

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

Aber sie KÖNNTEN es ein bisschen einfacher machen ... :)

Hallo .NET-Team! Nur berühren Basis zu diesem Thema. Es gibt es jetzt seit über einem Monat und ich bin nur neugierig, was das Team von dieser Diskussion hält.

Es gab verschiedene Punkte von beiden Seiten des Lagers - alle interessant und relevant meiner Meinung nach.

Gibt es Neuigkeiten von euch Mädels/Jungs?

dh.

  1. Wir haben die Convo gelesen und werden nichts ändern. Danke für deinen Beitrag, einen schönen Tag noch. Hier, nimm ein Stück :Kuchen:
  2. Wir haben ehrlich gesagt noch nicht darüber nachgedacht (hier herrscht wahnsinnig viel Betrieb, wie Sie sich vorstellen können) _aber_ wir werden zu einem späteren Zeitpunkt darüber nachdenken _bevor_ wir Release-To-Web (RTW) starten.
  3. Tatsächlich hatten wir einige interne Diskussionen darüber und wir erkennen an, dass die Öffentlichkeit es ein bisschen schmerzhaft findet, zu grok. Also denken wir, wir könnten XXXXXXXXX machen.

ta!

Es ist Teil 2 und 3.

Eine Möglichkeit, die Fähigkeit zum Einfügen eines „Mock“-Handlers zu vereinfachen, um beim Testen zu helfen, besteht darin, HttpClient eine statische Methode hinzuzufügen, dh HttpClient.DefaultHandler. Diese statische Methode ermöglicht es Entwicklern, beim Aufrufen des standardmäßigen HttpClient()-Konstruktors einen anderen Standardhandler als den aktuellen HttpClientHandler festzulegen. Zusätzlich zum Testen mit einem „Schein“-Netzwerk hilft es Entwicklern, die Code in portable Bibliotheken oberhalb von HttpClient schreiben, wo sie HttpClient-Instanzen nicht direkt erstellen. Es würde ihnen also erlauben, diesen alternativen Standardhandler zu "injizieren".

Dies ist also ein Beispiel für etwas, das wir dem aktuellen Objektmodell hinzufügen können, das additiv wäre und keine bahnbrechende Änderung an der aktuellen Architektur darstellt.

Vielen Dank @davidsh für die sehr schnelle Antwort :+1:

Ich werde glücklich warten und diesen Raum beobachten, da es aufregend ist zu wissen, dass es nicht Teil 1 ist :smile: .. und freue mich darauf zu sehen, was auch immer sich ändert :+1:

Nochmals vielen Dank (Team) für Ihre Geduld und Ihr Zuhören – ich weiß es wirklich sehr zu schätzen!

/me wartet glücklich.

@davidsh also, die virtuellen Methoden der Schnittstelle os kommen nicht in Frage?

Es kommt nicht in Frage. Das kurzfristige Hinzufügen von Schnittstellen, wie in diesem Thementhread vorgeschlagen, erfordert jedoch sorgfältiges Studium und Design. Es lässt sich nicht einfach dem vorhandenen Objektmodell von HttpClient zuordnen und wäre möglicherweise eine bahnbrechende Änderung an der API-Oberfläche. Es ist also nichts, was schnell oder einfach zu entwerfen/implementieren ist.

Was ist mit virtuellen Methoden?

Ja, das Hinzufügen virtueller Methoden ist keine Breaking Change, sondern eine additive. Wir arbeiten noch daran, welche Designänderungen in diesem Bereich machbar sind.

/me wirft Resurrection auf diesen Thread...

~ Faden dreht, stöhnt, zuckt und wird lebendig!


Ich habe gerade Community StandUp vom 20. August 2015 gehört und sie haben erwähnt, dass HttpClient plattformübergreifend mit Beta7 verfügbar sein wird. YAY!

Also ... gab es diesbezüglich irgendwelche Entscheidungen? Nicht entweder/oder vorschlagen ... nur höflich nach Diskussionsaktualisierungen usw. fragen?

@PureKrome Es gab keine Entscheidung darüber, aber diese API-Design-/Untersuchungsarbeit wird in unsere Zukunftspläne eingebunden. Im Moment konzentriert sich die Mehrheit von uns sehr darauf, den Rest des System.Net-Codes auf GitHub zu bringen und plattformübergreifend zu arbeiten. Dies umfasst sowohl bestehenden Quellcode als auch die Portierung unserer Tests auf das Xunit-Framework. Sobald diese Bemühungen zusammenlaufen, wird das Team mehr Zeit haben, sich auf zukünftige Änderungen zu konzentrieren, wie sie in diesem Thread vorgeschlagen werden.

Danke haufenweise @davidsh - Ich schätze wirklich Ihre Zeit, um zu antworten :) Machen Sie weiter mit der großartigen Arbeit! :Ballon:

Ist die Verwendung von Mocks zum Testen von HTTP-Diensten nicht etwas veraltet? Es gibt einige Self-Hosting-Optionen, die ein einfaches Nachahmen des HTTP-Dienstes ermöglichen und keine Codeänderungen erfordern, sondern nur eine Änderung der Dienst-URL. Dh HttpListener und OWIN selbst hosten, aber für komplexe APIs könnte man sogar einen Mock-Service bauen oder einen Autoresponder verwenden.

image

image

Gibt es ein Update zur Unit-Test-Fähigkeit dieser HttpClient-Klasse? Ich versuche, die DeleteAsync-Methode zu simulieren, aber wie bereits erwähnt, beschwert sich Moq darüber, dass die Funktion nicht virtuell ist. Es scheint keinen einfacheren Weg zu geben, als unseren eigenen Wrapper zu erstellen.

Ich stimme für Richardszalay.Mockhttp!
Es ist exzellent! Einfach und genial!

https://github.com/richardszalay/mockhttp

aber @YehudahA - wir sollten das nicht brauchen .... (das ist der Punkt dieser Convo) :)

@PureKrome wir sollten IHttpClient nicht brauchen.
Es ist die gleiche Idee wie bei EF 7. Sie verwenden nie IDbContext oder IDbSet, sondern ändern einfach das Verhalten mit DbContextOptions.

Richardszalay.MockHttp ist eine sofort einsatzbereite Lösung.

Es ist eine Out-of-Another-Box-Lösung.

wir sollten IHttpClient nicht brauchen.
Es ist dieselbe Idee wie bei EF 7. Sie verwenden nie IDbContext oder IDbSet, sondern ändern einfach das Verhalten mit DbContextOptions.

Das ist in Ordnung - ich bin völlig anderer Meinung, also einigen wir uns darauf, nicht einverstanden zu sein.

Funktionieren beide Wege? Jawohl. Einige Leute (wie ich) finden es aus Gründen wie Auffindbarkeit, Lesbarkeit und Einfachheit einfacher, eine Schnittstelle oder virtuelle Methode zu verspotten.

Andere (wie Sie selbst) manipulieren das Verhalten gerne. Ich finde das komplexer, schwieriger zu entdecken und zeitaufwändiger. (was (für mich) bedeutet, dass es schwieriger zu unterstützen / zu warten ist).

Jedem das Seine, denke ich :)

Richardszalay sagt: „Dieses Muster ist stark von $httpBackend von AngularJS inspiriert“.
Das ist richtig. Sehen Sie hier: https://docs.angularjs.org/api/ngMock/service/ $httpBackend

@PureKrome
Ja es ist einfacher.
Sie müssen nicht wissen, welche Klassen und Methoden verwendet werden, Sie brauchen keine Mock-Bibliothek/Schnittstellen/virtuelle Methoden, das Setup ändert sich nicht, wenn Sie von HttpClient zu etwas anderem wechseln.

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

Verwendungszweck:

            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 ist hier der beste, da er eine ordnungsgemäße Simulation des Verhaltens des Zieldienstes ermöglicht.

Es ist genau das, wonach ich gesucht habe, da es mir den perfekten Ort bietet, um „falsch geformte/bösartige“ Daten abzulegen (für Sicherheits- und Non-Happy-Path-Tests).

Ich bin zu spät zu diesem Thread gekommen, ich bin hier angekommen, nachdem ich gesucht hatte, wie Leute ihre Abhängigkeit von HttpClient testen, und einige sehr erfinderische Wege gefunden, dies zu tun. Ich werde vorweg sagen, dass ich in diesem Argument entschieden auf die Seite fallen würde, weil ich eine Schnittstelle habe. Das Problem für mich ist jedoch, dass ich keine Wahl habe.

Ich entscheide mich dafür, Komponenten und Dienste zu entwerfen und zu entwickeln, die ihre Abhängigkeit von anderen Komponenten erklären. Ich bevorzuge die Wahl, diese Abhängigkeiten über einen Konstruktor zu deklarieren und mich zur Laufzeit auf einen IoC-Container zu verlassen, um eine konkrete Implementierung einzufügen und an dieser Stelle zu steuern, ob es sich um ein Singleton oder eine vorübergehende Instanz handelt.

Ich entscheide mich für eine Definition von Testbarkeit, die ich vor etwa einem Jahrzehnt gelernt habe, als ich mich der damals relativ neuen Praxis der testgetriebenen Entwicklung zuwandte:

"Eine Klasse ist testbar, wenn sie aus ihrer normalen Umgebung entfernt werden kann und immer noch funktioniert, wenn alle ihre Abhängigkeiten eingefügt werden."

Eine andere Wahl, die ich lieber treffe, ist, die Interaktion meiner Komponenten mit denen zu testen, von denen sie abhängen. Wenn ich eine Abhängigkeit von einer Komponente deklariere, möchte ich testen, ob meine Komponente sie korrekt verwendet.

IMHO ermöglicht mir eine gut gestaltete API, so zu arbeiten und zu codieren, wie ich es wähle, einige technische Einschränkungen sind akzeptabel. Ich sehe nur eine konkrete Implementierung einer Komponente wie HttpClient als eine Möglichkeit, mir die Wahl zu nehmen.

Also bleibt mir die Wahl, in diesen Fällen auf meine übliche Lösung zurückzugreifen und eine leichte Adapter-/Wrapper-Bibliothek zu erstellen, die mir eine Schnittstelle gibt und es mir ermöglicht, so zu arbeiten, wie ich es möchte.

Ich bin auch etwas spät zur Party, aber selbst nachdem ich alle vorangegangenen Kommentare gelesen habe, bin ich mir immer noch nicht sicher, wie ich weiter vorgehen soll. Können Sie mir bitte sagen, wie ich überprüfen würde, ob bestimmte Methoden in meinem zu testenden Objekt bestimmte Endpunkte auf einer öffentlichen API aufrufen? Es ist mir egal, was zurückkommt. Ich möchte nur sicherstellen, dass, wenn jemand „GoGetSomething“ auf einer Instanz meiner ApiClient-Klasse aufruft, ein GET an „ https://somenifty.com/api/something “ gesendet wird. Was ich gerne tun möchte, ist (nicht funktionierender Code):

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"))));

Aber es lässt mich SendAsync nicht verspotten.

WENN ich diese grundlegende Sache zum Laufen bringen könnte, möchte ich vielleicht auch testen, wie meine Klasse mit bestimmten RETURNS von diesem Aufruf umgeht.

Irgendwelche Ideen? Dies scheint (für mich) nicht zu sehr aus dem Gleichgewicht zu sein, um es testen zu wollen.

Aber es lässt mich SendAsync nicht verspotten.

Sie spotten nicht über die Methode HttpClient.SendAsync .

Stattdessen erstellen Sie einen Mock-Handler, der von HttpMessageHandler abgeleitet ist. Dann überschreiben Sie die Methode SendAsync davon. Dann übergeben Sie dieses Handler-Objekt an den Konstruktor von HttpClient .

@jdcrutchley , Sie können dies mit MockHttp von @richardszalay tun.

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 oder Sie erstellen Ihre EIGENE Wrapper-Klasse und Wrapper-Schnittstelle mit einer Methode SendAsync .. die nur das _real_ httpClient.SendAsync (in Ihrer Wrapper-Klasse) aufruft.

Ich vermute, das ist es, was MockHttp von @richardszalay tut.

Das ist das allgemeine Muster, das Leute machen, wenn sie etwas nicht verspotten können (weil es keine Schnittstelle gibt ODER es nicht virtuell ist).

mockhttp ist ein Schein-DSL auf HttpMessageHandler. Es unterscheidet sich nicht vom dynamischen Mocking mit Moq, aber die Absicht ist aufgrund der DSL klarer.

Die Erfahrung ist in beiden Fällen gleich: HttpMessageHandler simulieren und daraus einen konkreten HttpClient erstellen. Ihre Domänenkomponente hängt entweder von HttpClient oder HttpMessageHandler ab.

Bearbeiten: TBH, ich bin mir nicht ganz sicher, was das Problem mit dem bestehenden Design ist. Der Unterschied zwischen „mockable HttpClient“ und „mockable HttpMessageHandler“ ist buchstäblich eine Codezeile in Ihrem Test: new HttpClient(mockHandler) , und es hat den Vorteil, dass die Implementierung dasselbe Design für „Unterschiedliche Implementierung pro Plattform“ und „ Andere Implementierung zum Testen" (d. h. ist nicht für die Zwecke des Testcodes befleckt).

Die Existenz von Bibliotheken wie MockHttp dient nur dazu, eine sauberere, domänenspezifische Test-API bereitzustellen. HttpClient über eine Schnittstelle verspottbar zu machen, würde daran nichts ändern.

Als Neuling in der HttpClient-API wird meine Erfahrung hoffentlich einen Mehrwert für die Diskussion darstellen.

Ich war auch überrascht zu erfahren, dass HttpClient keine Schnittstelle für einfaches Spotten implementiert hat. Ich habe im Internet gesucht, diesen Thread gefunden und ihn von Anfang bis Ende gelesen, bevor ich fortfahre. Die Antwort von @drub0y hat mich davon überzeugt, den Weg des einfachen Verspottens von HttpMessageHandler fortzusetzen.

Mein spöttisches Framework meiner Wahl ist Moq, wahrscheinlich ein ziemlicher Standard für .NET-Entwickler. Nachdem ich fast eine Stunde damit verbracht hatte, gegen die Moq-API zu kämpfen, indem ich versuchte, die geschützte SendAsync-Methode einzurichten und zu verifizieren, stieß ich schließlich auf einen Fehler in Moq, als ich geschützte generische Methoden verifizierte. Siehe hier .

Ich beschloss, diese Erfahrung zu teilen, um das Argument zu untermauern, dass eine einfache IHttpClient-Schnittstelle das Leben viel einfacher gemacht und mir ein paar Stunden gespart hätte. Jetzt, da ich in eine Sackgasse geraten bin, können Sie mich zu der Liste der Entwickler hinzufügen, die eine einfache Wrapper/Schnittstellen-Kombination um HttpClient herum geschrieben haben.

Das ist ein ausgezeichneter Punkt, Ken, ich hatte vergessen, dass HttpMessageHandler.SendAsync geschützt war.

Dennoch bleibe ich bei meiner Anerkennung, dass sich das API-Design zuerst auf die Plattformerweiterbarkeit konzentriert, und es ist nicht schwierig, eine Unterklasse von HttpMessageHandler zu erstellen (siehe mockhttps als Beispiel). Es könnte sogar eine Unterklasse sein, die den Aufruf an eine abstrakte öffentliche Methode weitergibt, die Mock-freundlicher ist. Mir ist klar, dass das "unrein" erscheint, aber Sie könnten für immer über Testbarkeit und Tainting-API-Oberfläche für Testzwecke streiten.

+1 für die Schnittstelle.

Wie andere bin ich ein Neuling bei HttpClient und brauche es in einer Klasse, begann mit der Suche nach Komponententests, entdeckte diesen Thread - und jetzt musste ich einige Stunden später effektiv etwas über die Implementierungsdetails lernen, um es zu testen. Die Tatsache, dass so viele diesen Weg gehen mussten, ist ein Beweis dafür, dass dies ein echtes Problem für die Menschen ist. Und wie viele von den wenigen, die sich zu Wort gemeldet haben, lesen den Thread nur schweigend?

Ich leite gerade ein großes Projekt in meiner Firma und habe einfach nicht die Zeit, solchen Kaninchenspuren nachzujagen. Eine Schnittstelle hätte mir Zeit gespart, meinem Unternehmen Geld – und das gilt vermutlich auch für viele andere.

Ich werde wahrscheinlich den Weg gehen, meinen eigenen Wrapper zu erstellen. Weil es von außen betrachtet für niemanden nachvollziehbar ist, warum ich einen HttpMessageHandler in meine Klasse eingefügt habe.

Wenn es nicht möglich ist, eine Schnittstelle für HttpClient zu erstellen, kann das .net-Team dann vielleicht eine ganz neue HTTP-Lösung erstellen, die eine Schnittstelle hat?

@scott-martin, HttpClient tut nichts, er sendet nur die Anfragen an HttpMessageHandler .

Anstatt HttpClient zu testen, können Sie HttpMessageHandler testen und verspotten. Es ist sehr einfach und Sie können auch richardszalay mockhttp verwenden.

@scott-martin, wie erwähnt, nur wenige Kommentare, Sie injizieren keinen HttpMessageHandler. Sie fügen HttpClient und _das_ mit HttpMessageHandler ein, wenn Sie Komponententests durchführen.

@richardszalay danke für den Tipp, das funktioniert besser, um Low-Level-Abstraktionen aus meinen Implementierungen herauszuhalten.

@YehudahA , ich konnte es zum Laufen bringen, indem ich den HttpMessageHandler verspottete. Aber "es zum Laufen zu bringen" ist nicht der Grund, warum ich mich eingemischt habe. Wie @PureKrome sagte, ist dies ein Problem der Auffindbarkeit. Wenn es eine Schnittstelle gegeben hätte, wäre ich in der Lage gewesen, sie zu verspotten und wäre in wenigen Minuten unterwegs gewesen. Da es keine gibt, musste ich mehrere Stunden damit verbringen, eine Lösung zu finden.

Ich verstehe die Einschränkungen und Zögern bei der Bereitstellung einer Schnittstelle. Ich möchte als Mitglied der Community nur mein Feedback geben, dass wir Schnittstellen mögen, und bitte geben Sie uns mehr davon - sie sind viel einfacher zu interagieren und zu testen.

dies ist ein Problem der Auffindbarkeit. Wenn es eine Schnittstelle gegeben hätte, wäre ich in der Lage gewesen, sie zu verspotten und wäre in wenigen Minuten unterwegs gewesen . Da es keine gibt, musste ich mehrere Stunden damit verbringen, eine Lösung zu finden.

:arrow_up: das. Darum geht es.

EDIT: Hervorhebung von mir.

Obwohl ich vollkommen zustimme, dass es besser auffindbar sein könnte, bin ich mir nicht sicher, wie es "mehrere Stunden" dauern könnte. Wenn Sie „mock httpclient“ googeln, wird diese Stack Overflow-Frage als erstes Ergebnis angezeigt, in der die beiden am höchsten bewerteten Antworten (obwohl gewährt, nicht die akzeptierte Antwort) HttpMessageHandler beschreiben.

NP - wir sind uns immer noch einig, nicht einverstanden zu sein. Es ist alles gut :)

Sie können dieses Problem schließen, da mscorlib mit all seinem Gepäck zurückkommt :)

Können wir diese httpclient-Klassen einfach eine Schnittstelle implementieren lassen? Den Rest können wir dann selbst erledigen.

Welche Lösung schlagen Sie vor, um den in einem DI-Dienst verwendeten HttpClient zu verspotten?
Ich versuche, einen FakeHttpClientHandler zu implementieren, und ich weiß nicht, was ich mit der SendAsync-Methode tun soll.

Ich habe einen einfachen GeoLocationService, der einen Microservice aufruft, um IP-basierte Standortinformationen von einem anderen abzurufen. Ich möchte den gefälschten HttpClientHandler an den Dienst übergeben, der zwei Konstruktoren hat, von denen einer die locationServiceAddress und einer die locationServiceAddress und den zusätzlichen HttpClientHandler erhält.

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

    }

In der Testklasse möchte ich eine interne Klasse namens FakeClientHandler schreiben, die HttpClientHandler erweitert und die SendAsync-Methode überschreibt. Eine andere Methode wäre, denke ich, die Verwendung eines Mock-Frameworks, um den HttpClientHandler zu verspotten. Ich weiß noch nicht, wie man das macht. Wenn du mir helfen könntest, würde ich für immer in deiner Schuld stehen.

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 Der HttpMessageHandler ist praktisch die "Implementierung". In der Regel erstellen Sie einen HttpClient, übergeben den Handler an den Konstruktor und lassen dann Ihre Dienstebene von HttpClient abhängen.

Die Verwendung eines generischen Mocking-Frameworks ist schwierig, da das überschreibbare SendAsync geschützt ist. Ich habe mockhttp speziell aus diesem Grund geschrieben (aber auch, weil es schön ist, domänenspezifische Spottfunktionen zu haben.

@danielmihai Was Richard gesagt hat, fasst ziemlich genau den _Schmerz_ zusammen, den wir alle gerade ertragen müssen, wenn wir uns über HttpClient lustig machen.

Es gibt auch HttpClient.Helpers als HttpMessageHandler Bibliothek, die Ihnen hilft, HttpClient zu verspotten.

Ich habe es herausgefunden, so etwas geschrieben ... [Ich glaube, ich habe mich mit dem 'Schmerz' befasst]

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

Alle Tests bestehen

Bitte kommentieren Sie die Implementierung, wenn es bessere Möglichkeiten gibt, dies zu tun.

Danke!

Ich füge meine Gedanken zu dieser mehr als ein Jahr alten Diskussion hinzu.

@SidarthNabar

Zunächst stelle ich fest, dass Sie neue Instanzen von HttpClient zum Senden jeder Anfrage erstellen – das ist nicht das beabsichtigte Entwurfsmuster für HttpClient. Das Erstellen einer HttpClient-Instanz und deren Wiederverwendung für alle Ihre Anforderungen trägt zur Optimierung des Verbindungspoolings und der Speicherverwaltung bei. Bitte erwägen Sie die Wiederverwendung einer einzelnen Instanz von HttpClient. Sobald Sie dies getan haben, können Sie den gefälschten Handler in nur eine Instanz von HttpClient einfügen, und Sie sind fertig.

Wo entsorge ich die nicht-injizierte Instanz, wenn ich keine using -Anweisung verwenden soll, sondern new meine HttpClient-Instanz z. B. im Konstruktor des Verbrauchers? Was ist mit dem HttpMessageHandler, den ich zum Einfügen in die HttpClient-Instanz konstruiere?

Abgesehen von der kompletten Schnittstelle-oder-nicht-Diskussion (ich bin ganz für Schnittstellen), könnte zumindest die Dokumentation angeben, dass Sie zu Testzwecken einen HttpMessageHandler injizieren können. Ja, der Konstruktor wird erwähnt, ja, es wird erwähnt, dass Sie Ihren eigenen HttpMessageHandler injizieren können, aber nirgendwo wird angegeben, warum ich dies tun möchte.

@richardszalay

Obwohl ich vollkommen zustimme, dass es besser auffindbar sein könnte, bin ich mir nicht sicher, wie es "mehrere Stunden" dauern könnte. Wenn Sie „mock httpclient“ googeln, wird diese Stack Overflow-Frage als erstes Ergebnis angezeigt, in der die beiden am höchsten bewerteten Antworten (obwohl gewährt, nicht die akzeptierte Antwort) HttpMessageHandler beschreiben.

Eine API sollte beim Durchsuchen der Dokumente intuitiv zu verwenden sein, IMHO. Google- oder SO-Fragen stellen zu müssen, ist sicherlich nicht intuitiv, die verknüpfte Frage, die weit vor .NET Core zurückreicht, macht die Aktualität nicht offensichtlich (vielleicht hat sich zwischen der Frage und der Veröffentlichung eines _neuen_ Frameworks etwas geändert?)

Vielen Dank an alle, die ihre Codebeispiele hinzugefügt und Hilfsbibliotheken bereitgestellt haben!

Ich meine, wenn wir eine Klasse haben, die einen http-Aufruf machen muss, und wir diese Klasse auf Komponenten testen wollen, dann müssen wir in der Lage sein, diesen http-Client zu verspotten. Da der http-Client keine Schnittstelle hat, kann ich keinen Mock für diesen Client einfügen. Stattdessen muss ich einen Wrapper (der eine Schnittstelle implementiert) für einen http-Client erstellen, der einen Message-Handler als optionalen Parameter akzeptiert, und ihm dann einen gefälschten Handler senden und diesen einfügen. Das sind eine ganze Menge zusätzlicher Reifen, durch die man umsonst springen kann. Die Alternative besteht darin, dass meine Klasse eine Instanz von HttpMessageHandler anstelle von HttpClient nimmt, denke ich ...

Ich will keine Haarspalterei betreiben, es fühlt sich einfach unangenehm an und wirkt etwas unintuitiv. Ich denke, die Tatsache, dass dies ein so beliebtes Thema ist, unterstützt dies.

Da der HTTP-Client keine Schnittstelle hat, können wir ihn nicht mit einem Container injizieren.

@dasjestyr Wir können Dependency Injection ohne Schnittstellen durchführen. Schnittstellen ermöglichen Mehrfachvererbung; Sie sind keine Voraussetzung für die Abhängigkeitsinjektion.

Ja, DI ist hier nicht das Problem. Es ist, wie einfach / schön es ist, es zu verspotten usw.

@shaunluttin Mir ist bewusst, dass wir DI ohne Schnittstelle machen können, ich habe nichts anderes impliziert; Ich spreche von einer Klasse, die eine Abhängigkeit von einem HttpClient hat. Ohne die Schnittstelle für den Client kann ich keinen verspotteten HTTP-Client oder einen beliebigen HTTP-Client injizieren (damit er isoliert getestet werden kann, ohne einen tatsächlichen HTTP-Aufruf zu tätigen), es sei denn, ich verpacke ihn in einen Adapter, der eine Schnittstelle implementiert, die ich ' ve definieren, um die des HttpClient nachzuahmen. Ich habe einige Formulierungen zur Verdeutlichung leicht geändert, falls Sie sich auf einen einzelnen Satz fixiert haben.

In der Zwischenzeit habe ich in meinen Komponententests einen gefälschten Nachrichtenhandler erstellt und diesen eingefügt, was bedeutet, dass meine Klassen darauf ausgelegt sind, einen Handler einzufügen, was meiner Meinung nach nicht das Schlimmste auf der Welt ist, sich aber unangenehm anfühlt. Und die Tests brauchen Fälschungen statt Mocks/Stubs. Grob.

Auch, akademisch gesprochen, ermöglichen Schnittstellen keine Mehrfachvererbung, sie erlauben Ihnen nur, sie nachzuahmen. Sie erben Schnittstellen nicht, Sie implementieren sie – Sie _erben_ keine Funktionalität, indem Sie Schnittstellen implementieren, sondern erklären nur, dass Sie die Funktionalität auf irgendeine Weise implementiert haben. Um die Mehrfachvererbung und einige bereits vorhandene Funktionen, die Sie an anderer Stelle geschrieben haben, anderweitig nachzuahmen, könnten Sie die Schnittstelle implementieren und die Implementierung an ein internes Mitglied weiterleiten, das die eigentliche Funktionalität enthält (z. B. einen Adapter), bei dem es sich tatsächlich um eine Komposition und nicht um eine Vererbung handelt.

In der Zwischenzeit habe ich in meinen Komponententests einen gefälschten Nachrichtenhandler erstellt und diesen eingefügt, was bedeutet, dass meine Klassen darauf ausgelegt sind, einen Handler einzufügen, was meiner Meinung nach nicht das Schlimmste auf der Welt ist, sich aber unangenehm anfühlt. Und die Tests brauchen Fälschungen statt Mocks/Stubs. Grob.

Das ist Herz/Kern dieser Ausgabe -> und es ist auch _rein_ rechthaberisch.

Eine Seite des Zauns: Handler für gefälschte Nachrichten, weil <insert their rational here>
Andere Seite des Zauns: Schnittstellen (oder sogar virtuelle Methoden, aber das ist immer noch ein bisschen PITA) wegen <insert their rational here> .

Nun, ich meine, Fälschungen erfordern viel zusätzliche Arbeit zum Einrichten und führen in vielen Fällen dazu, dass Sie eine ganz andere Klasse nur für den Test schreiben, die nicht wirklich viel Wert hinzufügt. Stubs sind großartig, weil sie es ermöglichen, den Test ohne eine echte Implementierung fortzusetzen. Mocks sind das, was mich wirklich interessiert, weil sie für Behauptungen verwendet werden können. Stellen Sie beispielsweise fest, dass die Methode in der Implementierung dieser Schnittstelle mit dem aktuellen Setup dieses Tests x-mal aufgerufen wurde.

Das musste ich tatsächlich in einem meiner Kurse testen. Dazu musste ich dem gefälschten Handler einen Akkumulator hinzufügen, der bei jedem Aufruf der SendAsync()-Methode inkrementiert wurde. Das war zum Glück ein einfacher Fall, aber komm schon. Spott ist an dieser Stelle praktisch ein Standard.

Yep - stimme dir hier vollkommen zu 👍 🍰

Bitte, Microsoft, fügen Sie den Punkt IHttpClient hinzu. ODER seien Sie konsequent und entfernen Sie alle Ihre Schnittstellen in der BCL - IList , IDictionary , ICollection , da sie sowieso "schwer zu versionieren" sind. Heck, werde auch IEnumerable los.

Im Ernst, anstatt zu argumentieren wie "Sie können einfach Ihren eigenen Handler schreiben" (sicher, aber eine Schnittstelle ist einfacher und sauberer) und "eine Schnittstelle führt zu bahnbrechenden Änderungen" (naja, das tun Sie sowieso in allen .net-Versionen) , fügen Sie einfach DIE SCHNITTSTELLE hinzu und lassen Sie die Entwickler entscheiden, wie sie sie testen möchten.

@lasseschou IList , IDictionary , ICollection und IEnumerable sind aufgrund der Vielzahl von Klassen, die sie implementieren, absolut kritisch. IHttpClient würde nur von HttpClient implementiert werden - um mit Ihrer Logik konsistent zu sein, müsste jeder einzelnen Klasse in der BCL ein Schnittstellentyp zugewiesen werden, da wir ihn möglicherweise verspotten müssen.
Ich wäre traurig, wenn ich diese Art von Unordnung sehen würde.

Sofern es keinen dramatischen Kompromiss gibt, ist IHttpClient sowieso die falsche Abstraktionsebene , um sich darüber lustig zu machen. Ihre App wird mit ziemlicher Sicherheit nicht den vollen Funktionsumfang von HttpClient nutzen. Ich würde vorschlagen, dass Sie HttpClient in Bezug auf die spezifischen Anforderungen Ihrer App in Ihren eigenen Dienst packen und Ihren eigenen Dienst verspotten.

@jnm2 - Aus Interesse, warum denkst du, dass ein IHttpClient die falsche Abstraktionsebene ist, um sich darüber lustig zu machen?

Ich hasse oder trolle nicht – ehrlich Q.

(Lerne gerne usw.).

Ich wollte dasselbe fragen, wenn man bedenkt, dass es so ziemlich eine perfekte Abstraktionsebene ist, da es sich um einen Client handelt.

Es ist kein Absolut. Wenn Sie einen Webbrowser erstellen, kann ich sehen, wie 1:1 zwischen dem, was Ihre App benötigt, und dem, was HttpClient abstrahiert, besteht. Aber wenn Sie HttpClient verwenden, um mit irgendeiner Art von API zu kommunizieren – irgendetwas mit spezifischeren Erwartungen zusätzlich zu HTTP – brauchen Sie nur einen Bruchteil dessen, was HttpClient leisten kann. Wenn Sie einen Dienst erstellen, der den HttpClient-Transportmechanismus kapselt und diesen Dienst simuliert, kann der Rest Ihrer App gegen die API-spezifische Schnittstellenabstraktion Ihres Dienstes codieren, anstatt gegen HttpClient zu codieren.

Außerdem unterschreibe ich das Prinzip "Verspotte niemals einen Typ, den du nicht besitzt", so YMMV.
Siehe auch http://martinfowler.com/articles/mocksArentStubs.html zum Vergleich zwischen klassischem Testen und Mocking.

Der Punkt hier ist, dass der HttpClient eine so große Weiterentwicklung darstellt
WebClient, XMLHttpRequest, dass dies die bevorzugte Methode für HTTP geworden ist
Anrufe. Es ist sauber und modern. Ich verwende immer nachahmbare Adapter, die sprechen
direkt an den HttpClient, sodass das Testen meines Codes einfach ist. Aber ich möchte
Testen Sie auch diese Adapterklassen. Und da entdecke ich: Es gibt keine
IHttpClient. Vielleicht hast du recht, es ist nicht nötig. Vielleicht denken Sie, es ist
die falsche Abstraktionsebene. Aber wirklich praktisch wäre es nicht
es in allen Projekten in eine weitere Klasse einpacken zu müssen. Ich sehe einfach nicht
keinen Grund, für eine so wichtige Klasse keine separate Schnittstelle zu haben.

Lasse Schu
Gründer

http://mouseflow.com
[email protected]

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

VERTRAULICHKEITSHINWEIS: Diese E-Mail-Nachricht, einschließlich aller Anhänge, ist
zur alleinigen Verwendung durch den/die beabsichtigten Empfänger und kann vertrauliche,
gesetzlich geschützte und/oder privilegierte Informationen. Irgendein
unbefugte Überprüfung, Verwendung, Offenlegung oder Verbreitung ist verboten. Wenn du
nicht der beabsichtigte Empfänger sind, wenden Sie sich bitte per Antwort-E-Mail an den Absender
und alle Kopien der Originalnachricht vernichten.

Am 12. November 2016 um 14:57 Uhr, Joseph Musser [email protected]
schrieb:

Es ist kein Absolut. Wenn Sie einen Webbrowser bauen, könnte ich sehen
wie es 1:1 zwischen dem ist, was Ihre App benötigt, und dem, was HttpClient abstrahiert. Aber
wenn Sie HttpClient verwenden, um mit irgendeiner Art von API zu kommunizieren - irgendetwas mit
Wenn Sie spezifischere Erwartungen zusätzlich zu HTTP haben, benötigen Sie nur a
Bruchteil dessen, was HttpClient tun kann. Wenn Sie einen Dienst bauen, der
kapselt den HttpClient-Transportmechanismus und verspottet diesen Dienst, die
Der Rest Ihrer App kann mit der API-spezifischen Schnittstelle Ihres Dienstes codieren
Abstraktion, anstatt gegen HttpClient zu codieren.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/dotnet/corefx/issues/1624#issuecomment -260123552 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/ACnGPp3fSriEJCAjXeAqMZbBcTWcRm9Rks5q9cXlgaJpZM4EPBHy
.

Ich höre eine seltsame Eingabe, dass der HttpClient "zu flexibel" ist, um die richtige Schicht zum Spotten zu sein - das scheint eher ein Nebeneffekt der Entscheidung zu sein, dass eine Klasse so viele Verantwortlichkeiten und exponierte Funktionen hat. Aber es ist wirklich unabhängig davon, wie die Leute den Code testen _wollen_.

Ich bin immer noch sehr zuversichtlich, dass wir dies beheben, und sei es nur, weil wir die Kapselung aufheben, indem wir erwarten, dass Verbraucher diese Details zur Implementierung von HttpClient kennen (in diesem Fall „wissen“, dass es sich um einen Wrapper handelt, der keine sinnvolle Funktionalität hinzufügt und umschließt eine andere Klasse, die sie möglicherweise verwenden oder nicht). Framework-Konsumenten sollten nichts über die Implementierung wissen müssen, außer genau, welche Schnittstelle ihnen ausgesetzt ist, und ein Großteil der Rationalisierung hier ist, dass wir eine Problemumgehung haben und dass eine tiefe Vertrautheit mit der Klasse viele nicht gut gekapselte Möglichkeiten bietet Mocking / Stubbing-Funktionalität.

Bitte korrigieren!

Ich bin gerade darüber gestolpert, also werfe ich meine 2c hinein ...

Ich denke, HttpClient zu verspotten, ist Torheit.

Auch wenn es nur ein einziges SendAsync zu verspotten gibt, gibt es _so_ viele Nuancen zu HttpResponseMessage , HttpResponseHeaders , HttpContentHeaders usw werden schnell chaotisch und verhalten sich wahrscheinlich auch falsch.

Mir? Ich verwende dazu OwinHttpMessageHandler

  1. Führen Sie HTTP-Akzeptanztests außerhalb eines Dienstes durch.
  2. Fügen Sie _Dummy_-HTTP-Dienste in Komponenten ein, die ausgehende HTTP-Anforderungen an diese Dienste senden möchten. Das heißt, meine Komponente hat einen optionalen ctor-Parameter für HttpMessageHandler .

Ich bin mir sicher, dass es einen MVC6-Handler gibt, der ähnliches tut ....

_Das Leben ist in Ordnung._

@damianh : Warum sollten Leute, die dotnet core verwenden, irgendetwas mit Owin tun müssen, um eine Klasse wie HttpClient zu testen? Dies kann für einen bestimmten Anwendungsfall sehr sinnvoll sein (AspCore wird auf OWIN gehostet), aber dies scheint nicht so allgemein zu sein.

Dies scheint wirklich die Sorge zu verschmelzen, "was der allgemeinste Weg zum Komponententest von HttpClient ist, der auch mit den Benutzererwartungen übereinstimmt" mit "wie mache ich einen Integrationstest" :).

Oh _testing_ HttpClient ... dachte, das Thema wäre das Testen von Dingen, die eine haben
Abhängigkeit von HttpClient. Kümmern Sie sich nicht um mich. :)

Am 5. Dezember 2016 um 19:56 Uhr schrieb "brphelps" [email protected] :

@damianh https://github.com/damianh : Warum sollten Leute dotnet core verwenden
müssen Sie etwas mit Owin tun, um eine Klasse wie HttpClient zu testen? Das könnte
für einen bestimmten Anwendungsfall sehr sinnvoll (AspCore gehostet auf OWIN), aber
das scheint nicht so allgemein zu sein.

Dies scheint wirklich die Sorge zu verschmelzen, "was das Allgemeinste ist
Weg zum Unit Test HttpClient, der auch den Benutzererwartungen entspricht"
mit "Wie mache ich einen Integrationstest" :).


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/dotnet/corefx/issues/1624#issuecomment-264942511 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AADgXJxunxBgFGLozOsisCoPqjMoch48ks5rFF5vgaJpZM4EPBHy
.

@damianh - Es sind Dinge, die eine Abhängigkeit haben, aber Ihre Lösung scheint auch viele andere Abhängigkeiten im Bild zu beinhalten. Ich stelle mir eine Klassenbibliothek vor, die zufällig eine übergebene HttpClient-Instanz (oder eine DI-Instanz) verwendet. Wo kommt Owin ins Spiel?

Das ist nicht das Interessante, was ich zu zeigen versuchte (was
funktioniert auch auf .net core / netstandard), also konzentrieren Sie sich bitte nicht darauf.

Das Interessante ist der HttpMessageHandler, der einen In-Process macht
In-Memory-HTTP-Anfrage gegen Dummy-HTTP-Endpunkte.

Der TestServer von Aspnet Core funktioniert genauso wie der Katana-Server davor
Weg.

Ihre zu testende Klasse hat also eine HttpClient-DI, wo _it_ eine hat
HttpMessageHandler direkt injiziert. Das prüfbare und
durchsetzbare Oberfläche ist da.

Mocking, eine spezielle Form des Testdoubles, würde ich dennoch empfehlen
gegen für HttpClient. Daher ist kein IHttpClient erforderlich.

Am 5. Dezember 2016 um 20:15 Uhr schrieb "brphelps" [email protected] :

@damianh https://github.com/damianh – Es sind Dinge, die ein haben
Abhängigkeit, aber Ihre Lösung scheint viele andere Abhängigkeiten zu beinhalten
auch auf dem bild. Ich stelle mir eine Klassenbibliothek vor, die zufällig a verwendet
übergeben in der HttpClient-Instanz (oder einer DI-Instanz) - Wo kommt Owin ins Spiel?
das Bild?


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/dotnet/corefx/issues/1624#issuecomment-264947538 , oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AADgXEESfQj9NnKLo8LucFLyajWtQXKBks5rFGK8gaJpZM4EPBHy
.

@damianh - Verstehen Sie vollkommen, was Sie sagen, das Interessante ist - aber die Leute brauchen keine Dummy-HTTP-Endpunkte mit einem intelligenten Mock - z. B. die MockHttp -Bibliothek von @richardszalay .

Zumindest nicht für Unit-Tests :).

Es hängt davon ab, ob. Optionen sind gut :)

Am 5. Dezember 2016 um 20:39 Uhr schrieb "brphelps" [email protected] :

@damianh https://github.com/damianh – Verstehe vollkommen, was du bist
Das Interessante ist -- aber die Leute brauchen kein Dummy-HTTP
Endpunkte mit einem intelligenten Schein – z. B. @richardszalay
Die MockHttp-Bibliothek von https://github.com/richardszalay .

Zumindest nicht für Unit-Tests :).


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/dotnet/corefx/issues/1624#issuecomment-264954210 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AADgXOl4UEDGYCVLpbvwhueaK52VtjH6ks5rFGhlgaJpZM4EPBHy
.

@damianh sagte:
Optionen sind gut :)

Yep - dem kann ich voll und ganz zustimmen 👍 Welches, IMO .. haben wir gerade nicht 😢 **

Ich verstehe die Gründe für die Erstellung und Injektion Ihres eigenen HMH vollkommen. Das mache ich gerade. _Leider_ muss ich einen 2. ctor erstellen (oder einen optionalen ctor-Parameter angeben) _aus Gründen des Unit-Tests_. Denken Sie nur noch einmal über diesen Satz nach -> ich erstelle eine durchaus mögliche USER OPTION ... die wirklich nur dazu dient, einem Komponententest zu ermöglichen, _Zeug_ zu machen. Ein Endbenutzer/Verbraucher meiner Klasse wird niemals _wirklich_ diese ctor-Option verwenden ... was für mich stinkt.

Zurück zu den Optionen -> im Moment habe ich nicht das Gefühl, dass wir eine haben. Wir _müssen_ etwas über die Interna von HttpClient _lernen_. Öffnen Sie buchstäblich die Motorhaube und schauen Sie unter die Motorhaube. (Ich hatte das Glück, eine TL;DR; von Sir Fowler The Awesome ™️ zu bekommen, was meine Recherchezeit deutlich verkürzt hat).

Also - mit Ihrem Argument, keine Schnittstelle zu sein, können Sie bitte einige Beispiele dafür geben, wann eine Schnittstelle eine schlechte Sache wäre, bitte? usw. Denken Sie daran -> Ich habe immer eine Schnittstelle als _Option_ für kleine/einfache bis mittlere Beispiele beworben ... nicht 100% aller Beispiele.

Ich versuche nicht, dich zu trollen oder anzugreifen, Damian – ich versuche nur zu verstehen.

* Ninja Edit: Technisch gesehen haben wir tatsächlich Optionen -> wir können nichts tun / unsere eigenen Wrapper erstellen / etc. Ich meinte: nicht viele Optionen, die die Dinge vereinfachen, * out-of-the-box .

Hey @PureKrome , keine Sorge, nur Erfahrungsaustausch.

Der Begriff „Einheitentest“ wurde mehrfach verwendet, und lassen Sie uns kurz darüber nachdenken. Wir sind also auf der gleichen Seite, ich nehme an, wir haben ein FooClass , das eine ctor-Abhängigkeit von HttpClient (oder HMH ) hat, und wir wollen _mock_ HttpClient .

Was wir hier wirklich sagen, ist: „ FooClass kann eine HTTP-Anforderung unter Verwendung einer beliebigen URL (Protokoll/Host/Ressource) mit jeder Methode, jedem Inhaltstyp, jeder Übertragungscodierung, jeder Inhaltscodierung, _any Anforderungsheader_ und kann jeden Antwortinhaltstyp, jeden Statuscode, Cookies, Umleitungen, Übertragungscodierung, Cache-Steuerungsheader usw. sowie Netzwerkzeitüberschreitungen, Ausnahmen für abgebrochene Aufgaben usw. verarbeiten.

HttpClient ist wie ein Gottesobjekt ; man kann viel damit machen. HTTP ist auch ein Remote-Service-Aufruf, der über Ihren Prozess hinausreicht.

Ist das wirklich Unit -Testing? Sei jetzt ehrlich :)

** Auch Ninja Edit: Wenn Sie FooClass Unit testbar machen wollen, wo es JSON von einem Server bekommen wollte, hätte es eine Ctor-Abhängigkeit von Func<CancellationToken, Task<JObject>> oder ähnlichem.

Können Sie bitte einige Beispiele nennen, wann eine Schnittstelle eine schlechte Sache wäre?

Das habe ich nicht gesagt! Ich sagte, IHttpClient wäre nicht nützlich, weil A) es nicht durch eine alternative konkrete Implementierung ersetzt wird und B) das Gottobjekt betrifft, wie erwähnt.

Ein anderes Beispiel? IAssembly .

@damianh -- Der Status "God Class" ist für Anrufer kein Problem. Es liegt nicht in der Verantwortung des Aufrufers sicherzustellen, dass HttpClient eine Gottklasse ist/nicht ist. Sie wissen, dass es das Objekt ist, mit dem sie HTTP-Aufrufe tätigen :).

Angenommen, wir akzeptieren, dass das .NET-Framework uns eine Gott-Klasse offengelegt hat ... wie können wir unsere Bedenken reduzieren? Wie können wir es am besten kapseln? Das ist ganz einfach – Verwenden Sie ein traditionelles Mocking-Framework (Moq ist ein großartiges Beispiel) und isolieren Sie die HttpClient-Implementierung vollständig, da es uns als Nutzern einfach egal ist, wie es intern gestaltet ist . Uns interessiert nur, wie wir es verwenden. Das Problem hier ist, dass der herkömmliche Ansatz nicht funktioniert und als Ergebnis sehen Sie, dass die Leute immer interessantere Lösungen finden, um das Problem zu lösen :).

Einige Änderungen für Grammatik und Klarheit :D

Stimme @damianh voll und ganz zu, dass HttpClient wie eine übergroße Gottklasse ist.

Ich sehe auch nicht ein, warum ich noch einmal über all die Interna Bescheid wissen muss, weil es _so viele Verwandlungen_ geben kann, was hineingeht und was herauskommt.

Zum Beispiel: Ich möchte die Aktien von einer API abrufen. https://api.stock-r-us.com/aapl . Das Ergebnis ist ein JSON-Paylod.

Also, Dinge, die ich vermasseln könnte, wenn ich versuche, diesen Endpunkt anzurufen:

  • falsche Endpunkt-URL?
  • falsches Schema?
  • fehlende bestimmte Kopfzeile(n)?
  • falsche Überschriften
  • falsche Anforderungsnutzlast (vielleicht ist dies ein POST mit etwas Körper?)
  • Netzwerkfehler/Timeout/etc
    ...

Ich verstehe also, dass, wenn ich auf diese Dinge testen möchte, das HMH eine großartige Möglichkeit ist, dies zu tun, weil es einfach das _echte_ HMH entführt und ersetzt, was ich gesagt habe, um zurückzukehren.

Aber - was ist, wenn ich (auf eigene Gefahr) all diese Dinge ignorieren und einfach sagen möchte:
_Stellen Sie sich vor, ich möchte einen Endpunkt anrufen, alles ist in Ordnung und ich bekomme mein schönes JSON-Payload-Ergebnis._
Sollte ich all diese Dinge noch lernen und mir Sorgen machen, um so etwas Einfaches zu tun?

Oder ... ist das eine schlechte Denkweise. Wir sollten niemals so denken, weil es zu hacky ist? zu billig? zu fehleranfällig?

Meistens rufe ich nur einen Endpunkt an. Ich denke nicht an Dinge wie Header oder Cache-Kontrolle oder Netzwerkfehler. Normalerweise denke ich nur an VERB + URL (und PAYLOAD, falls erforderlich) und dann an das ERGEBNIS. Ich habe eine Fehlerbehandlung, um all die Dinge zu fangen :tm: und mich dann geschlagen zu geben, weil etwas Schlimmes passiert ist :(

Also betrachte ich das Problem entweder falsch (dh ich kann meinen n00b-Status immer noch nicht abschütteln) – oder – diese Gottklasse verursacht uns einen Haufen Unit-Testing-Trauer.

HTTP ist auch ein Remote-Service-Aufruf, der über Ihren Prozess hinausreicht.
Ist das wirklich Unit-Testing? Sei jetzt ehrlich :)

Für mich ist HttpClient ein _abhängiger Dienst_. Es ist mir egal, _was_ der Dienst wirklich tut, ich klassifiziere es als _etwas, das einige Dinge tut, die außerhalb meiner Codebasis liegen_. Daher möchte ich nicht, dass es Dinge mit einer externen Stromversorgung macht, also möchte ich das kontrollieren, es kapern und die Ergebnisse dieser Service-Anrufe bestimmen. Ich versuche immer noch, _meine_ Arbeitseinheit zu testen. Mein kleines Stück Logik. Kann natürlich auch von anderen Sachen abhängen. Es ist immer noch ein Einheitentest, ob ich das gesamte Universum kontrollieren kann, in dem dieses Stück Logik existiert _und_ ich mich nicht auf andere Dienste verlassen/integrieren muss.

** Noch eine Bearbeitung: Oder vielleicht - wenn HttpClient tatsächlich ein Gottobjekt ist, sollte das Gespräch vielleicht darum gehen, es auf etwas ... Besseres zu reduzieren? Oder vielleicht ist es überhaupt kein Gottesobjekt? (Ich versuche nur, die Debatte zu eröffnen).

Leider muss ich einen 2. ctor erstellen (oder einen optionalen ctor-Parameter angeben) für Unit-Testing-Bedenken. Denken Sie nur noch einmal über diesen Satz nach -> ich erstelle eine durchaus mögliche USER OPTION ... die wirklich nur dazu dient, einem Komponententest zu ermöglichen, Dinge zu tun. Ein Endbenutzer/Verbraucher meiner Klasse wird diese ctor-Option niemals wirklich verwenden ... was für mich stinkt

Ich bin mir nicht sicher, was Sie hier sagen wollen:

"Ich mag es nicht, einen zweiten Ctor zu haben, der HttpMessageHandler akzeptiert" ... also lassen Sie Ihre Tests ihn in einen HttpClient einschließen

"Ich möchte einen Komponententest durchführen, ohne meine Abhängigkeit von HttpClient umzukehren" ... schwierig?

@richardszalay -- Ich glaube, ich habe das Gefühl, dass @PureKrome seinen Code nicht so ändern möchte, dass das Design nicht verbessert wird, nur um eine übermäßig eigensinnige Teststrategie zu unterstützen. Verstehen Sie mich nicht falsch, ich beschwere mich nicht über Ihre Bibliothek, der einzige Grund, warum ich eine bessere Lösung möchte, ist, dass ich eine höhere Erwartung habe, dass dotnet core sie nicht benötigt =).

Ich denke nicht an MockHttp, sondern an Testen/Mocken im Allgemeinen.
Invertierte Abhängigkeiten sind eine grundlegende Notwendigkeit für Unit-Tests in .NET, und das beinhaltet normalerweise eine Ctor-Injektion. Mir ist nicht klar, was die vorgeschlagene Alternative wäre.

Wenn HttpClient IHttpClient implementiert, gäbe es sicherlich immer noch einen Konstruktor, der es akzeptiert ...

Wenn HttpClient IHttpClient implementiert, gäbe es sicherlich immer noch einen Konstruktor, der es akzeptiert ...

Einverstanden. Ich „kreuzte Ströme“ in meinen Gedanken, als ich buchstäblich gerade aufwachte und darum kämpfte, meine einzelne Zelle in meinem Gehirn zum Leben zu erwecken.

Ignoriere bitte meinen 'Ctor'-Kommentar und fahre mit der Diskussion fort, falls nicht schon jemand aus Langeweile gegangen ist 😄

Können wir einen Schritt zurück zu der Frage machen, ob die Verwendung HttpClient als Abhängigkeit wirklich zu Unit-Tests führen _kann_.

Nehmen wir an, ich habe eine Klasse FooClass , die einen Abhängigkeitskonstruktor injiziert bekommt. Ich möchte nun die Methoden auf FooClass einem Unit-Test unterziehen, d. h. ich spezifiziere die Eingabe für meine Methode, ich isoliere FooClass so gut ich kann von seiner Umgebung und prüfe das Ergebnis gegen einen erwarteten Wert . In all dieser Pipeline möchte ich mich nicht um die Eingeweide meines abhängigen Objekts kümmern; Am besten wäre es für mich, wenn ich einfach die Art von Umgebung angeben könnte, in der mein FooClass leben soll: Ich möchte die Abhängigkeit von meinem zu testenden System so verspotten, dass es sich genau so verhält, wie ich es brauche damit meine Tests funktionieren.

Jetzt muss ich mich um die Art und Weise kümmern, wie HttpClient implementiert wird, da ich eine Abhängigkeit ( HttpMessageHandler ) für meine injizierte Abhängigkeit ( HttpClient ) für mein zu testendes System angeben muss. Anstatt meine direkte Abhängigkeit nur zu verspotten, muss ich eine konkrete Umsetzung davon mit einer verspotteten verketteten Abhängigkeit schaffen.

Thema: Ich kann HttpClient nicht verspotten, ohne zu wissen, wie seine Interna funktionieren, und ich muss mich auch auf seine Implementierung verlassen, um nichts Überraschendes zu tun, da ich die Interna der Klasse durch Verspotten nicht vollständig umgehen kann. Dies ist im Grunde nicht das Befolgen von Unit Testing 101 -- Abrufen von nicht verwandtem Abhängigkeitscode aus Ihren Tests =).

Sie können das Thema in vielen verschiedenen Kommentaren sehen:

"Für mich ist HttpClient ein abhängiger Dienst. Es ist mir egal, was der Dienst wirklich tut, ich klassifiziere ihn als etwas, das einige Dinge tut, die außerhalb meiner Codebasis liegen." -- PureKrome

"Jetzt muss ich mich um die Art und Weise kümmern, wie HttpClient implementiert wird, weil ich eine Abhängigkeit (HttpMessageHandler) für meine injizierte Abhängigkeit (HttpClient) für mein zu testendes System angeben muss." -- Thaoden

„Ich wollte dasselbe fragen, wenn man bedenkt, dass es so ziemlich eine perfekte Abstraktionsebene ist, da es sich um einen Client handelt.“ -- dasjestyr

"Der HttpMessageHandler ist praktisch die "Implementierung". Sie erstellen normalerweise einen HttpClient, übergeben den Handler an den Constuctor und lassen dann Ihre Dienstschicht von HttpClient abhängen." - Richardszalay

„Aber das Hauptproblem wird bleiben – Sie müssen einen gefälschten Handler definieren und ihn unter dem HttpClient-Objekt einfügen.“ -- Sidharth Nabar

„HttpClient hat viele Optionen für die Unit-Testbarkeit. Es ist nicht versiegelt und die meisten seiner Mitglieder sind virtuell. Es hat eine Basisklasse, die verwendet werden kann, um die Abstraktion zu vereinfachen, sowie einen sorgfältig entworfenen Erweiterungspunkt in HttpMessageHandler“ – ericstj

„Wenn die API nicht mockbar ist, sollten wir sie beheben. Aber es hat nichts mit Schnittstellen oder Klassen zu tun. - KrzysztofCwalina

„Bei allem Respekt, wenn die Option darin besteht, einen eigenen Faked Message Handler zu schreiben, was ohne Durchforsten des Codes nicht offensichtlich ist, dann ist das eine sehr hohe Hürde, die für viele Entwickler enorm frustrierend sein wird.“ -- ctolkien

Ist das wirklich Unit-Testing? Sei jetzt ehrlich :)

Ich weiß nicht. Wie sieht die Prüfung aus?

Ich denke, die Aussage ist ein bisschen wie ein Strohmann. Der Punkt ist, die Abhängigkeit zu verspotten, die der http-Client ist, damit der Code isoliert von diesen Abhängigkeiten ausgeführt werden kann ... mit anderen Worten, nicht wirklich einen http-Aufruf tätigen. Wir würden lediglich sicherstellen, dass die erwarteten Methoden auf der Schnittstelle unter den richtigen Bedingungen aufgerufen werden. Ob wir eine leichtere Abhängigkeit hätten wählen sollen oder nicht, scheint eine ganz andere Diskussion zu sein.

HttpClient ist die schlimmste Abhängigkeit aller Zeiten. Es heißt wörtlich , dass die
Angehörige können "im Internet anrufen". Sicher, ihr werdet als nächstes TCP verspotten. Heh

Am 7. Dezember 2016 um 5:34 Uhr schrieb „Jeremy Stafford“ [email protected] :

Ist das wirklich Unit-Testing? Sei jetzt ehrlich :)

Ich weiß nicht. Wie sieht die Prüfung aus?

Ich denke, die Aussage ist ein bisschen wie ein Strohmann. Der Punkt ist, die zu verspotten
Abhängigkeit, die der HTTP-Client ist. Wir würden nur dafür sorgen
die erwarteten Methoden auf der Schnittstelle wurden rechts aufgerufen
Bedingungen. Ob wir eine leichtere Abhängigkeit hätten wählen sollen oder nicht, scheint
eine ganz andere Diskussion sein.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/dotnet/corefx/issues/1624#issuecomment-265353526 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AADgXPL39vnUbWrUuUBtfmbjhk5IBkxHks5rFjdqgaJpZM4EPBHy
.

Dies ist im Grunde nicht das Befolgen von Unit Testing 101 -- Abrufen von nicht verwandtem Abhängigkeitscode aus Ihren Tests =).

Ich persönlich glaube nicht, dass das die richtige Interpretation dieser speziellen Praxis ist. Wir versuchen nicht, die Abhängigkeit zu testen, wir versuchen zu testen, wie unser Code mit dieser Abhängigkeit interagiert , also ist es nicht unabhängig. Und ganz einfach: Wenn ich keinen Komponententest für meinen Code ausführen kann, ohne die eigentliche Abhängigkeit aufzurufen, dann ist das kein isolierter Test. Wenn dieser Komponententest beispielsweise auf einem Build-Server ohne Internetzugang ausgeführt würde, würde der Test wahrscheinlich fehlschlagen – daher der Wunsch, ihn zu verspotten. Sie könnten die Abhängigkeit von HttpClient entfernen, aber das bedeutet nur, dass es in einer anderen Klasse landen wird, die Sie schreiben, die den Aufruf durchführt - vielleicht spreche ich von dieser Klasse? Du weißt es nicht, weil du nie gefragt hast. Sollte mein Domänendienst also eine Abhängigkeit von HttpClient haben? Nein natürlich nicht. Es würde zu einer anderen Art von abstraktem Dienst abstrahiert, und ich würde mich stattdessen darüber lustig machen. Ok, also in der Schicht, die jede beliebige Abstraktion einfügt, die mir einfällt – irgendwo in der Kette wird _etwas_ über HttpClient Bescheid wissen und dass etwas von etwas anderem umschlossen wird, das ich schreibe, und dass etwas anderes etwas von mir ist werde noch einen Unit-Test für schreiben.

Also, wenn mein Einheitentest für eine Art Mediator testet, dass ein nicht erfolgreicher Aufruf eines Dienstes im Internet dazu führt, dass mein Code eine Art Transaktion rückgängig macht, im Vergleich zu einem erfolgreichen Aufruf eines Dienstes, der dazu führt, dass dies ausgeführt wird Transaktion, dann muss ich die Kontrolle über das Ergebnis dieses Anrufs haben, um die Bedingungen dieses Tests so festzulegen, dass ich bestätigen kann, dass sich beide Szenarien korrekt verhalten.

Ich konnte die Meinung einiger verstehen, dass HttpClient nicht die richtige Abstraktionsebene ist, aber ich glaube nicht, dass jemand nach der "Ebene" der konsumierenden Klasse gefragt hat, was irgendwie mutmaßlich ist. Man könnte den HTTP-Aufruf an einen Dienst abstrahieren und diesen anbinden, aber einige von uns möchten trotzdem testen, ob sich unsere „Wrapper“ für diesen HTTP-Aufruf wie erwartet verhalten, was bedeutet, dass wir die Antwort kontrollieren müssen. Derzeit kann dies erreicht werden, indem HttpClient ein gefälschter Handler gegeben wird, was ich jetzt mache, und das ist in Ordnung, aber der Punkt ist, dass Sie, wenn Sie diese Abdeckung wollen, irgendwann mit HttpClient herumfummeln müssen zumindest die Antwort vortäuschen.

Aber jetzt lass mich zurückrudern. Je mehr ich es mir ansehe, es ist viel zu fett für eine spöttische Schnittstelle (was gesagt wurde). Ich denke, es kann eine mentale Sache sein, die sich aus dem Namen "Client" ergibt. Ich sage nicht, dass ich es für falsch halte, aber es stimmt eher nicht mit anderen "Client"-Implementierungen überein, die man heutzutage in freier Wildbahn sehen kann. Wir sehen heute viele „Clients“ da draußen, die wirklich Servicefassaden sind. Wenn also jemand den Namen Http CLIENT sieht, denken sie vielleicht dasselbe, und warum sollten Sie sich nicht über einen Service lustig machen, oder?

Ich sehe jetzt, dass der Handler der Teil ist, der für den Code wichtig ist, also werde ich in Zukunft darum herum entwerfen.

Grundsätzlich denke ich, dass unser Wunsch, HttpClient zu verspotten, in gewisser Weise mit der Neigung des durchschnittlichen Entwicklers zusammenhängt, neue Instanzen von HttpClient zu erstellen, anstatt sie wiederzuverwenden, was eine schreckliche Praxis ist. Mit anderen Worten, wir unterschätzen, was HttpClient ist, und sollten uns stattdessen auf den Handler konzentrieren. Außerdem ist der Handler abstrakt, sodass er leicht verspottet werden kann.

Okay, ich bin verkauft. Ich möchte die Schnittstelle auf HttpClient nicht mehr zum Spotten verwenden.

Nachdem ich viel zu lange gekämpft hatte, habe ich meine eigene IHttpHandler-Schnittstelle erstellt und alles für meinen Anwendungsfall implementiert. Macht meinen konkreten Code leichter lesbar und Tests können ohne irgendwelchen gruseligen Arschkram verspottet werden.

Es ist nahezu unmöglich, eine Instanz von HttpClient in einer wirklichen Anwendung zu teilen, sobald Sie bei jeder Anfrage einen anderen HTTP-Header senden müssen (was bei der Kommunikation mit richtig gestalteten RESTful-Webdiensten entscheidend ist). Derzeit ist HttpRequestHeaders DefaultRequestHeaders an die Instanz und nicht an einen Aufruf gebunden, wodurch sie effektiv zustandsbehaftet ist. Stattdessen sollte {Method}Async() es akzeptieren, was HttpClient zustandslos und wirklich wiederverwendbar machen würde.

@abatishchev Aber Sie können Header für jede HttpRequestMessage angeben.

@richardszalay Ich sage nicht, dass es völlig unmöglich ist, ich sage, dass HttpClient für diesen Zweck nicht gut entwickelt wurde. Keiner von {Method}Async() akzeptiert HttpRequestMethod , nur SendAsync() tut es. Aber was ist der Zweck des Rests dann?

Aber was ist der Zweck des Rests dann?

Erfüllung von 99 % der Anforderungen.

Bedeutet das, dass das Setzen von Headern 1 % der Anwendungsfälle ausmacht? Ich bezweifle.

In beiden Fällen ist dies kein Problem, wenn diese Methoden eine Überladung hatten, die HttpResponseMessage akzeptiert.

@abatishchev Ich bezweifle es nicht, aber so oder so würde ich Erweiterungsmethoden schreiben, wenn ich mich in Ihrem Szenario wiederfinden würde.

Ich spielte mit der Idee, vielleicht HttpMessageHandler und möglicherweise HttpRequestMessage zu verbinden, weil ich es nicht mochte, Fälschungen schreiben zu müssen (vs. Mocks). Aber je weiter Sie diesen Kaninchenbau hinuntergehen, desto mehr erkennen Sie, dass Sie versuchen werden, tatsächliche Datenwertobjekte (z. B. HttpContent) zu fälschen, was eine vergebliche Übung ist. Daher denke ich, dass es am besten geeignet ist, Ihre abhängigen Klassen so zu entwerfen, dass sie optional HttpMessageHandler als ctor-Argument verwenden und eine Fälschung für Komponententests verwenden. Ich würde sogar argumentieren, dass das Umhüllen von HttpClient auch Zeitverschwendung ist ...

Auf diese Weise können Sie Ihre abhängige Klasse testen, ohne tatsächlich einen Anruf im Internet zu tätigen, was Sie möchten. Und Ihre Fälschung kann vordefinierte Statuscodes und Inhalte zurückgeben, sodass Sie testen können, ob Ihr abhängiger Code sie wie erwartet verarbeitet, was wiederum das ist, was Sie tatsächlich wollen.

@dasjestyr Hast du versucht, eine Schnittstelle für HttpClient zu erstellen (was wie das Erstellen eines Wrappers dafür ist) anstelle von Schnittstellen für HttpMessageHandler oder HttpRequestMessage ..

/ bin neugierig.

@PureKrome Ich habe skizziert, wie man eine Schnittstelle dafür erstellt, und dort wurde mir schnell klar, dass es sinnlos war. HttpClient abstrahiert wirklich nur eine Menge Dinge, die im Kontext von Komponententests keine Rolle spielen, und ruft dann den Nachrichtenhandler auf (was ein Punkt war, der in diesem Thread einige Male angesprochen wurde). Ich habe auch versucht, einen Wrapper dafür zu erstellen, und das war einfach nicht die Arbeit wert, die erforderlich war, um ihn zu implementieren oder die Praxis zu verbreiten (dh "yo, alle tun dies, anstatt HttpClient direkt zu verwenden"). Es ist VIEL einfacher, sich einfach auf den Handler zu konzentrieren, da er Ihnen alles gibt, was Sie brauchen, und buchstäblich aus einer einzigen Methode besteht.

Allerdings habe ich meinen eigenen RestClient erstellt, der jedoch ein anderes Problem gelöst hat, nämlich die Bereitstellung eines Fluid-Request-Builders, aber selbst dieser Client akzeptiert einen Message-Handler, der für Unit-Tests oder zur Implementierung benutzerdefinierter Handler verwendet werden kann, die Dinge wie Handler-Ketten verarbeiten die Querschnittsprobleme lösen (z. B. Protokollierung, Authentifizierung, Wiederholungslogik usw.), was der richtige Weg ist. Das ist nicht spezifisch für meinen Rest-Client, das ist nur ein großartiger Anwendungsfall zum Festlegen des Handlers. Aus diesem Grund gefällt mir die HttpClient-Schnittstelle im Windows-Namespace eigentlich viel besser, aber ich schweife ab.

Ich denke, es könnte immer noch nützlich sein, den Handler zu verbinden, aber es müsste dort aufhören. Ihr Mocking-Framework kann dann so eingerichtet werden, dass es vordefinierte Instanzen von HttpResponseMessage zurückgibt.

Interessant. Ich habe festgestellt (persönliche Voreingenommenheit?), dass meine Hilfsbibliothek hervorragend funktioniert, wenn ein (gefälschter) konkreter Nachrichtenhandler verwendet wird.

Ich würde es trotzdem vorziehen, diese Bibliothek nicht schreiben oder verwenden zu müssen :)

Ich sehe kein Problem darin, eine kleine Bibliothek zu erstellen, um Fälschungen zu erstellen. Ich könnte das tun, wenn ich mich langweile, weil ich nichts anderes zu tun habe. Alle meine HTTP-Sachen sind bereits abstrahiert und getestet, sodass ich im Moment keine wirkliche Verwendung dafür habe. Ich sehe einfach keinen Wert darin, den HttpClient zum Zweck des Komponententests zu verpacken. Den Handler vorzutäuschen ist alles, was Sie wirklich brauchen. Die Erweiterung der Funktionalität ist ein völlig separates Thema.

Wenn der größte Teil der Codebasis mithilfe von Mocking-Schnittstellen getestet wird, ist es bequemer und konsistenter, wenn der Rest der Codebasis auf die gleiche Weise getestet wird. Also würde ich gerne eine Schnittstelle IHttpClient sehen. Wie IFileSystemOperarions von ADL SDK oder IDataFactoryManagementClient von ADF Management SDK usw.

Ich denke immer noch, dass Sie den Punkt vermissen, der HttpClient muss nicht verspottet werden, nur der Handler. Das eigentliche Problem ist die Art und Weise, wie die Leute HttpClient betrachten. Es ist keine zufällige Klasse, die neu erstellt werden sollte, wenn Sie daran denken, das Internet anzurufen. Tatsächlich empfiehlt es sich, den Client in Ihrer gesamten Anwendung wiederzuverwenden – das ist eine so große Sache. Ziehen Sie die beiden Dinge auseinander, und es macht mehr Sinn.

Außerdem sollten sich Ihre abhängigen Klassen nicht um den HttpClient kümmern, sondern nur um die Daten, die von ihm zurückgegeben werden – die vom Handler stammen. Stellen Sie sich das so vor: Werden Sie jemals die HttpClient-Implementierung durch etwas anderes ersetzen? Möglich... aber nicht wahrscheinlich. Sie müssen die Funktionsweise des Clients nie ändern, warum sollten Sie sich also die Mühe machen, dies zu abstrahieren? Der Message-Handler ist die Variable. Sie möchten ändern, wie die Antworten behandelt werden, aber nicht, was der Client tut. Sogar die Pipeline in der WebAPI konzentriert sich auf den Handler (siehe: Handler delegieren). Je öfter ich es sage, desto mehr fange ich an zu denken, dass .Net den Client statisch machen und ihn für Sie verwalten sollte ... aber ich meine ... was auch immer.

Denken Sie daran, wofür Schnittstellen sind. Sie sind nicht zum Testen da – es war nur ein cleverer Weg, sie zu nutzen. Schnittstellen nur für diesen Zweck zu erstellen, ist lächerlich. Microsoft hat Ihnen alles gegeben, was Sie brauchen, um das Nachrichtenbehandlungsverhalten zu entkoppeln, und es funktioniert perfekt zum Testen. Eigentlich ist HttpMesageHandler abstrakt, daher denke ich, dass die meisten spöttischen Frameworks wie Moq immer noch damit funktionieren würden.

Heh @dasjestyr - Ich denke auch, dass Sie einen wichtigen Punkt meiner Diskussion verpasst haben könnten.

Die Tatsache, dass wir (der Entwickler) so viel über Message Handler usw. _lernen_ müssen, um die Antwort zu fälschen, ist mein _Hauptpunkt_ bei all dem. Nicht so sehr über Schnittstellen. Sicher, ich (persönlich) bevorzuge Schnittstellen zu virtuellen Wrappern in Bezug auf Tests (und daher Mocking) ... aber das sind Implementierungsdetails.

Ich hoffe, dass der Hauptinhalt dieses epischen Threads darin besteht, hervorzuheben, dass ... wenn Sie HttpClient in einer Anwendung verwenden, es ein PITA ist, um damit zu testen.

Der Status quo von „Lernen Sie die Installation von HttpClient, was Sie zu HttpMessageHandler usw. führt“ ist eine schlechte Situation. Für viele andere Bibliotheken usw. müssen wir dies nicht tun.

Also hatte ich gehofft, dass etwas getan werden kann, um dieses PITA zu lindern.

Ja, die PITA ist eine Meinung – das gebe ich voll und ganz zu. Manche Leute denken, es ist überhaupt kein PITA usw.

Das eigentliche Problem ist die Art und Weise, wie die Leute HttpClient betrachten. Es ist keine zufällige Klasse, die neu erstellt werden sollte, wenn Sie daran denken, das Internet anzurufen.

Einverstanden! Aber bis vor kurzem war dies nicht bekannt - was zu einem Mangel an Dokumentation oder Unterricht oder so etwas führen könnte.

Außerdem sollten sich Ihre abhängigen Klassen nicht um den HttpClient kümmern, sondern nur um die Daten, die von ihm zurückgegeben werden

Ja - einverstanden.

das kommt vom Handler.

ein Was? hm? Oh -- jetzt bittest du mich, die Motorhaube zu öffnen und etwas über die Klempnerarbeit zu lernen? ... Siehe oben immer wieder.

Stellen Sie sich das so vor: Werden Sie jemals die HttpClient-Implementierung durch etwas anderes ersetzen?

Nein.

.. etc ..

Nun, ich möchte nicht trollen usw. Also denken Sie bitte nicht, dass ich versuche, ein Idiot zu sein, indem ich immer wieder antworte und das Gleiche wiederhole.

Ich benutze meine Hilfsbibliothek seit Ewigkeiten, was nur eine verherrlichte Art ist, einen benutzerdefinierten Nachrichtenhandler zu verwenden. Toll! Es funktioniert und funktioniert super. Ich denke nur, dass dies _alles_ ein bisschen schöner dargestellt werden könnte ... und ich hoffe, dass dieser Thread wirklich darauf ankommt.

EDIT: Formatierung.

(Ich habe den Grammatikfehler im Titel dieser Ausgabe gerade erst bemerkt und kann ihn jetzt nicht mehr übersehen.)

Und das war mein _echtes_ langes Spiel :)

QED

(eigentlich - so peinlich 😊 )

EDIT: Ein Teil von mir will es nicht bearbeiten ... aus Nostalgie ....

(Ich habe den Grammatikfehler im Titel dieser Ausgabe gerade erst bemerkt und kann ihn jetzt nicht mehr übersehen.)

Ich kenne! Dasselbe!

Die Tatsache, dass wir (der Entwickler) so viel über Message Handler usw. lernen müssen, um die Antwort zu fälschen, ist mein Hauptpunkt bei all dem. Nicht so sehr über Schnittstellen. Sicher, ich (persönlich) bevorzuge Schnittstellen zu virtuellen Wrappern in Bezug auf Tests (und daher Mocking) ... aber das sind Implementierungsdetails.

Was?

Hast du dir schon mal HttpMessageHandler angeschaut? Es ist eine abstrakte Klasse, die buchstäblich eine einzelne Methode ist, die eine HttpRequestMessage nimmt und eine HttpResponseMessage zurückgibt – gemacht, um Verhalten getrennt von all dem Low-Level-Transportmüll abzufangen, was genau das ist, was Sie in einem Komponententest wollen. Um es vorzutäuschen, implementieren Sie es einfach. Die eingehende Nachricht ist diejenige, die Sie an HttpClient gesendet haben, und die Antwort, die ausgegeben wird, liegt bei Ihnen. Wenn ich beispielsweise wissen möchte, dass mein Code korrekt mit einem JSON-Text umgeht, dann lasse die Antwort einfach einen JSON-Text zurückgeben, den du erwartest. Wenn Sie sehen möchten, ob 404 korrekt verarbeitet wird, lassen Sie 404 zurückgeben. Grundlegender geht es nicht. Um den Handler zu verwenden, senden Sie ihn einfach als ctor-Argument für HttpClient. Sie müssen keine Kabel herausziehen oder die internen Abläufe von irgendetwas lernen.

Ich hoffe, dass der Hauptinhalt dieses epischen Threads darin besteht, hervorzuheben, dass ... wenn Sie HttpClient in einer Anwendung verwenden, es ein PITA ist, um damit zu testen.

Und der Kern dessen, was viele Leute darauf hingewiesen haben, ist, dass Sie es falsch machen (weshalb es ein PITA ist, und ich sage das direkt, aber respektvoll) und zu fordern, dass HttpClient zu Testzwecken mit einer Schnittstelle versehen wird, ist vergleichbar mit dem Erstellen von Features Kompensieren Sie Fehler, anstatt das Grundproblem anzugehen, das in diesem Fall darin besteht, dass Sie auf der falschen Ebene arbeiten.

Ich denke, dass HttpClient im Windows-Namespace das Konzept des Handlers tatsächlich in filter getrennt hat, aber es macht genau dasselbe. Ich denke, dass der Handler/Filter in dieser Implementierung tatsächlich eine Schnittstelle hat, was ich früher vorgeschlagen habe.

Was?
Hast du dir schon mal HttpMessageHandler angeschaut?

Zunächst nein, deshalb:

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

Das heißt, die exponierten Methoden auf HttpClient sind wirklich nett und machen die Dinge _wirklich_ einfach :) Yay! Schnell, einfach, funktioniert, GEWINNEN!

Ok - also lass es uns jetzt in einem Test verwenden ... und jetzt müssen wir Zeit damit verbringen, zu lernen, was darunter passiert ... was uns dazu bringt, etwas über HttpMessageHandler usw. zu lernen.

Ich sage das immer wieder => dieses zusätzliche Lernen über die Installation von HttpClient (dh HMH usw.) ist eine PITA. Sobald Sie _das gelernt haben_ .. ja, dann weiß ich, wie man es benutzt usw. ... obwohl ich es auch nicht gerne weiter benutze, aber muss, um Remote-Anrufe an Endpunkte von Drittanbietern zu simulieren. Klar, das ist eigensinnig. Dem können wir nur zustimmen. Also bitte - ich kenne HMH und wie man es benutzt.

Und die Hauptaussage dessen, worauf viele Leute hingewiesen haben, ist, dass Sie es falsch machen

Yep - die Leute sind anderer Meinung als ich. Kein Problem. Außerdem - die Leute stimmen mir auch zu. Wieder keine Probs.

und _fordern_, dass HttpClient zu Testzwecken mit einer Schnittstelle versehen wird, ist vergleichbar mit dem Erstellen von Funktionen zum Kompensieren von Fehlern, anstatt das Grundproblem anzugehen, das in diesem Fall darin besteht, dass Sie auf der falschen Ebene arbeiten.

Ok - ich widerspreche Ihnen hier respektvoll (was in Ordnung ist). Wieder unterschiedliche Meinungen.

Aber srl. Dieser Thread läuft schon so lange. Mittlerweile bin ich wirklich weiter gekommen. Ich sagte mein Stück, manche Leute sagen JA! Manche sagten NEIN! Nichts hat sich geändert und ich habe einfach ... den Status quo akzeptiert und mit allem gerollt.

Es tut mir leid, dass Sie meinen Gedankengang einfach nicht akzeptieren. Ich bin kein schlechter Mensch und versuche, nicht unhöflich oder gemein zu klingen. Hier habe ich nur zum Ausdruck gebracht, wie ich mich während der Entwicklung gefühlt habe und wie ich glaube, dass andere sich auch gefühlt haben. Das ist alles.

Ich bin überhaupt nicht beleidigt. Ich bin ursprünglich mit der gleichen Meinung auf diesen Thread gestoßen, dass der Client verspottbar sein sollte, aber dann wurden einige wirklich gute Punkte darüber gemacht, was HttpClient ist, also habe ich mich hingesetzt und wirklich darüber nachgedacht und dann versucht, es selbst herauszufordern, und schließlich ich kamen zu dem gleichen Schluss, nämlich dass HttpClient das Falsche ist, sich darüber lustig zu machen, und dass der Versuch, dies zu tun, eine vergebliche Übung ist, die weit mehr Arbeit verursacht, als es wert ist. Das zu akzeptieren, machte das Leben viel einfacher. Also bin auch ich weitergezogen. Ich hoffe nur, dass andere sich irgendwann eine Pause gönnen und es so nehmen, wie es ist.

Nebenbei:

Zunächst nein, deshalb:

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

Das heißt, die exponierten Methoden auf HttpClient sind wirklich nett und machen die Dinge wirklich einfach :) Yay! Schnell, >einfach, funktioniert, GEWINNEN!

Ich würde argumentieren, dass diese Schnittstelle in Bezug auf SOLID sowieso ungeeignet ist. IMO-Client, Anfrage, Antwort sind 3 verschiedene Verantwortlichkeiten. Ich kann die Bequemlichkeitsmethoden auf dem Client schätzen, aber es fördert eine enge Kopplung, indem Anfragen und Antworten mit dem Client kombiniert werden, aber das ist eine persönliche Präferenz. Ich habe einige Erweiterungen für HttpResponseMessage, die dasselbe erreichen, aber die Verantwortung für das Lesen der Antwort mit der Antwortnachricht behalten. Meiner Erfahrung nach ist „Simple“ bei großen Projekten nie „Simple“ und endet fast immer in einem BBOM. Das ist aber eine ganz andere Diskussion :)

Da wir jetzt ein neues Repo speziell für Designdiskussionen haben, setzen Sie die Diskussion bitte unter https://github.com/dotnet/designs/issues/9 fort oder öffnen Sie ein neues Issue unter https://github.com/dotnet/designs

Danke.

Vielleicht könnte die folgende Lösung nur zu Testzwecken in Betracht gezogen werden:

öffentliche Klasse GeoLocationServiceForTest : GeoLocationService, IGeoLocationService
{
public GeoLocationServiceForTest(etwas: etwas, HttpClient httpClient) : base(etwas)
{
base._httpClient = httpClient;
}
}

Am Ende habe ich MockHttp von @richardszalay verwendet .
Danke, Richard, du hast meinen Tag gerettet!

Ok, ich kann mit einem HttpClient ohne Schnittstelle leben. Aber gezwungen zu sein, eine Klasse zu implementieren, die HttpMessageHandler erbt, weil SendAsync geschützt ist, ist einfach scheiße.

Ich benutze NSubstitute, und das funktioniert nicht:

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)

Wirklich enttäuschend.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

yahorsi picture yahorsi  ·  3Kommentare

omariom picture omariom  ·  3Kommentare

v0l picture v0l  ·  3Kommentare

matty-hall picture matty-hall  ·  3Kommentare

Timovzl picture Timovzl  ·  3Kommentare