HttpClient in Unit-Tests verspotten

108

Ich habe einige Probleme beim Versuch, meinen Code für Unit-Tests zu verpacken. Das Problem ist das. Ich habe die Schnittstelle IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

Und die Klasse, die es benutzt, HttpHandler:

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

Und dann die Connection-Klasse, die simpleIOC verwendet, um die Client-Implementierung einzufügen:

public class Connection
{
    private IHttpHandler _httpClient;

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

Und dann habe ich ein Unit-Test-Projekt mit dieser Klasse:

private IHttpHandler _httpClient;

[TestMethod]
public void TestMockConnection()
{
    var client = new Connection(_httpClient);

    client.doSomething();  

    // Here I want to somehow create a mock instance of the http client
    // Instead of the real one. How Should I approach this?     

}

Jetzt werde ich natürlich Methoden in der Connection-Klasse haben, die Daten (JSON) von meinem Back-End abrufen. Ich möchte jedoch Komponententests für diese Klasse schreiben, und natürlich möchte ich keine Tests gegen das echte Back-End schreiben, sondern gegen ein verspottetes. Ich habe versucht, eine gute Antwort darauf zu googeln, ohne großen Erfolg. Ich kann und habe Moq benutzt, um mich vorher zu verspotten, aber niemals auf so etwas wie httpClient. Wie soll ich dieses Problem angehen?

Danke im Voraus.

tjugg
quelle
1
Wenn Sie ein HttpClientin Ihrer Benutzeroberfläche anzeigen , liegt das Problem darin. Sie zwingen Ihren Client, die HttpClientkonkrete Klasse zu verwenden. Stattdessen sollten Sie eine Abstraktion des HttpClient.
Mike Eason
Können Sie es etwas ausführlicher erklären? Wie soll ich den Verbindungsklassenkonstruktor erstellen, da ich keine Abhängigkeiten von HttpClient in anderen Klassen möchte, die die Verbindungsklasse verwenden? Zum Beispiel möchte ich keinen konzertierten HttpClient im Konstruktor von Connection übergeben, da dadurch jede andere Klasse, die Connection verwendet, von HttpClient abhängig wird.
Tjugg
Was haben Sie aus Interesse googelt? Anscheinend könnte mockhttp einige SEO-Verbesserungen gebrauchen.
Richard Szalay
@Mike - wie in meiner Antwort erwähnt, besteht keine Notwendigkeit, HttpClient zu abstrahieren. Es ist so wie es ist perfekt testbar. Ich habe zahlreiche Projekte, die Backend-freie Testsuiten mit dieser Methode haben.
Richard Szalay

Antworten:

37

Ihre Schnittstelle macht die konkrete HttpClientKlasse verfügbar . Daher sind alle Klassen, die diese Schnittstelle verwenden, daran gebunden. Dies bedeutet, dass sie nicht verspottet werden kann.

HttpClienterbt nicht von einer Schnittstelle, so dass Sie Ihre eigene schreiben müssen. Ich schlage ein dekoratorisches Muster vor:

public interface IHttpHandler
{
    HttpResponseMessage Get(string url);
    HttpResponseMessage Post(string url, HttpContent content);
    Task<HttpResponseMessage> GetAsync(string url);
    Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}

Und deine Klasse wird so aussehen:

public class HttpClientHandler : IHttpHandler
{
    private HttpClient _client = new HttpClient();

    public HttpResponseMessage Get(string url)
    {
        return GetAsync(url).Result;
    }

    public HttpResponseMessage Post(string url, HttpContent content)
    {
        return PostAsync(url, content).Result;
    }

    public async Task<HttpResponseMessage> GetAsync(string url)
    {
        return await _client.GetAsync(url);
    }

    public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
    {
        return await _client.PostAsync(url, content);
    }
}

Der Punkt bei all dem ist, dass HttpClientHandleres seine eigenen erstellt HttpClient. Sie können dann natürlich mehrere Klassen erstellen, die IHttpHandlerauf unterschiedliche Weise implementiert werden.

Das Hauptproblem bei diesem Ansatz ist , dass Sie tatsächlich eine Klasse schreiben , die Methoden in einer anderen Klasse nur Anrufe, jedoch könnten Sie eine Klasse , die erbt von HttpClient(Siehe Nkosi des Beispiel , es ist ein viel besserer Ansatz als meins). Das Leben wäre viel einfacher, wenn man HttpClienteine Schnittstelle hätte, die man verspotten könnte, leider nicht.

Dieses Beispiel ist jedoch nicht das goldene Ticket. IHttpHandlerstützt sich immer noch auf HttpResponseMessage, was zum System.Net.HttpNamespace gehört. Wenn Sie also andere Implementierungen als benötigen HttpClient, müssen Sie eine Art Mapping durchführen, um deren Antworten in HttpResponseMessageObjekte zu konvertieren . Dies ist natürlich nur dann ein Problem, wenn Sie mehrere Implementierungen von verwenden müssen, IHttpHandleraber es sieht nicht so aus, als ob Sie dies tun. Es ist nicht das Ende der Welt, aber es ist etwas, worüber Sie nachdenken müssen.

Wie auch immer, Sie können sich einfach lustig machen, IHttpHandlerohne sich um die konkrete HttpClientKlasse kümmern zu müssen, da sie abstrahiert wurde.

Ich empfehle, die nicht asynchronen Methoden zu testen , da diese immer noch die asynchronen Methoden aufrufen, ohne sich um das Testen von asynchronen Methoden durch Einheiten kümmern zu müssen (siehe hier)

Mike Eason
quelle
Dies beantwortet in der Tat meine Frage. Die Nkosis-Antwort ist auch richtig, daher bin ich mir nicht sicher, welche ich als Antwort akzeptieren soll, aber ich werde mit dieser gehen. Vielen Dank für die Mühe
tjugg
@tjugg Freut mich zu helfen. Stimmen Sie die Antworten ab, wenn Sie sie als nützlich erachtet haben.
Nkosi
3
Es ist erwähnenswert, dass der Hauptunterschied zwischen dieser Antwort und der von Nkosi darin besteht, dass dies eine viel dünnere Abstraktion ist. Dünn ist wahrscheinlich gut für ein bescheidenes Objekt
Ben Aaronson
225

Die Erweiterbarkeit von HttpClient liegt in der HttpMessageHandlerÜbergabe an den Konstruktor. Es ist beabsichtigt, plattformspezifische Implementierungen zuzulassen, aber Sie können es auch verspotten. Es ist nicht erforderlich, einen Dekorations-Wrapper für HttpClient zu erstellen.

Wenn Sie ein DSL der Verwendung von Moq vorziehen, habe ich eine Bibliothek auf GitHub / Nuget, die die Dinge etwas einfacher macht: https://github.com/richardszalay/mockhttp

var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
Richard Szalay
quelle
1
Also würde ich MockHttpMessageHandler einfach als Messagehandler-Httphandler-Klasse übergeben? Oder wie haben Sie es in Ihren eigenen Projekten
implementiert
2
Tolle Antwort und etwas, das ich anfangs nicht gewusst hätte. Macht die Arbeit mit HttpClient nicht so schlecht.
Bealer
6
Für Leute, die sich nicht mit dem Injizieren des Clients befassen möchten, aber dennoch eine einfache Testbarkeit wünschen, ist es trivial, dies zu erreichen. Ersetzen Sie einfach var client = new HttpClient()mit var client = ClientFactory()und Einrichtung ein Feld internal static Func<HttpClient> ClientFactory = () => new HttpClient();und auf der Testebene können Sie dieses Feld neu zu schreiben.
Chris Marisic
3
@ChrisMarisic Sie schlagen eine Form des Servicestandorts vor, um die Injektion zu ersetzen. Der Servicestandort ist ein bekanntes Anti-Muster, daher ist eine Imho-Injektion vorzuziehen.
MarioDS
2
@MarioDS und unabhängig davon sollten Sie überhaupt keine HttpClient- Instanz injizieren . Wenn Sie nicht bereit sind, dafür die Konstruktorinjektion zu verwenden, sollten Sie eine HttpClientFactorywie in injizieren Func<HttpClient>. Da ich HttpClient als reines Implementierungsdetail und nicht als Abhängigkeit betrachte, verwende ich die Statik wie oben dargestellt. Ich bin völlig in Ordnung mit Tests, die Interna manipulieren. Wenn mir Pure-ism am Herzen liegt, stehe ich auf vollen Servern und teste Live-Code-Pfade. Wenn Sie irgendeine Art von Schein verwenden, akzeptieren Sie eine Annäherung an das Verhalten, nicht an das tatsächliche Verhalten.
Chris Marisic
39

Ich stimme einigen anderen Antworten zu, dass der beste Ansatz darin besteht, HttpMessageHandler zu verspotten, anstatt HttpClient zu verpacken. Diese Antwort ist insofern einzigartig, als sie weiterhin HttpClient injiziert, sodass es sich um einen Singleton handelt oder mit Abhängigkeitsinjektion verwaltet wird.

"HttpClient soll einmal instanziiert und während der gesamten Lebensdauer einer Anwendung wiederverwendet werden." ( Quelle ).

Das Verspotten von HttpMessageHandler kann etwas schwierig sein, da SendAsync geschützt ist. Hier ist ein vollständiges Beispiel mit xunit und Moq.

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq

namespace MockHttpClient {
    class Program {
        static void Main(string[] args) {
            var analyzer = new SiteAnalyzer(Client);
            var size = analyzer.GetContentSize("http://microsoft.com").Result;
            Console.WriteLine($"Size: {size}");
        }

        private static readonly HttpClient Client = new HttpClient(); // Singleton
    }

    public class SiteAnalyzer {
        public SiteAnalyzer(HttpClient httpClient) {
            _httpClient = httpClient;
        }

        public async Task<int> GetContentSize(string uri)
        {
            var response = await _httpClient.GetAsync( uri );
            var content = await response.Content.ReadAsStringAsync();
            return content.Length;
        }

        private readonly HttpClient _httpClient;
    }

    public class SiteAnalyzerTests {
        [Fact]
        public async void GetContentSizeReturnsCorrectLength() {
            // Arrange
            const string testContent = "test content";
            var mockMessageHandler = new Mock<HttpMessageHandler>();
            mockMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync(new HttpResponseMessage {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(testContent)
                });
            var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));

            // Act
            var result = await underTest.GetContentSize("http://anyurl");

            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}
PointZeroTwo
quelle
1
Das hat mir sehr gut gefallen. Das mockMessageHandler.Protected()war der Mörder. Danke für dieses Beispiel. Es ermöglicht das Schreiben des Tests, ohne die Quelle zu ändern.
Tyrion
1
Zu Ihrer Information, Moq 4.8 unterstützt stark typisierte Verspottungen geschützter Mitglieder - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay
2
Das sieht gut aus. Auch Moq unterstützt ReturnsAsync, so dass der Code aussehen würde.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord
Danke @kord, das habe ich der Antwort hinzugefügt
PointZeroTwo
3
Gibt es eine Möglichkeit zu überprüfen, ob "SandAsync" mit einigen Parametern aufgerufen wurde? Ich habe versucht, ... Protected (). Verify (...) zu verwenden, aber es sieht so aus, als würde es mit asynchronen Methoden nicht funktionieren.
Rroman
29

Dies ist eine häufige Frage, und ich wollte unbedingt HttpClient verspotten, aber ich glaube, ich bin endlich zu der Erkenntnis gekommen, dass Sie HttpClient nicht verspotten sollten. Es scheint logisch, dies zu tun, aber ich denke, wir wurden von Dingen einer Gehirnwäsche unterzogen, die wir in Open-Source-Bibliotheken sehen.

Wir sehen oft "Clients" da draußen, die wir in unserem Code verspotten, damit wir isoliert testen können. Deshalb versuchen wir automatisch, dasselbe Prinzip auf HttpClient anzuwenden. HttpClient macht tatsächlich viel; Sie können sich das als Manager für HttpMessageHandler vorstellen, also wollen Sie sich darüber nicht lustig machen, und deshalb hat es immer noch keine Schnittstelle. Der Teil, an dem Sie für Unit-Tests oder sogar für das Entwerfen Ihrer Services wirklich interessiert sind, ist der HttpMessageHandler, da dies die Antwort zurückgibt, und Sie können sich darüber lustig machen .

Es ist auch erwähnenswert, dass Sie wahrscheinlich anfangen sollten, HttpClient wie eine größere Sache zu behandeln. Zum Beispiel: Halten Sie die Installation neuer HttpClients auf ein Minimum. Verwenden Sie sie wieder, sie sind so konzipiert, dass sie wiederverwendet werden können, und verbrauchen eine Menge weniger Ressourcen, wenn Sie dies tun. Wenn Sie anfangen, es wie ein größeres Geschäft zu behandeln, wird es sich viel falscher anfühlen, wenn Sie es verspotten wollen, und jetzt wird der Nachrichtenhandler das sein, was Sie injizieren, nicht der Client.

Mit anderen Worten, entwerfen Sie Ihre Abhängigkeiten um den Handler herum anstatt um den Client. Noch besser, abstrakte "Dienste", die HttpClient verwenden, mit denen Sie einen Handler injizieren können, und diesen stattdessen als injizierbare Abhängigkeit verwenden. Dann können Sie in Ihren Tests den Handler fälschen, um die Reaktion zum Einrichten Ihrer Tests zu steuern.

Das Einwickeln von HttpClient ist eine verrückte Zeitverschwendung.

Update: Siehe das Beispiel von Joshua Dooms. Genau das empfehle ich.

Sinaesthetic
quelle
17

Wie auch in den Kommentaren erwähnt müssen Sie abstrakt weg die HttpClientes so nicht zu koppeln. Ich habe in der Vergangenheit etwas Ähnliches gemacht. Ich werde versuchen, das, was ich getan habe, an das anzupassen, was Sie versuchen zu tun.

Schauen Sie sich zuerst die HttpClientKlasse an und entscheiden Sie, welche Funktionalität sie benötigt.

Hier ist eine Möglichkeit:

public interface IHttpClient {
    System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
    System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
    System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
    System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}

Wie bereits erwähnt, war dies wiederum für bestimmte Zwecke. Ich habe die meisten Abhängigkeiten von allem, was damit zu tun hat, vollständig abstrahiert HttpClientund mich auf das konzentriert, was ich zurückgeben wollte. Sie sollten bewerten, wie Sie das abstrahieren möchten, HttpClientum nur die erforderliche Funktionalität bereitzustellen.

Auf diese Weise können Sie jetzt nur das verspotten, was zum Testen benötigt wird.

Ich würde sogar empfehlen, ganz abzuschaffen IHttpHandlerund die HttpClientAbstraktion zu verwenden IHttpClient. Aber ich wähle einfach nicht aus, da Sie den Hauptteil Ihrer Handler-Schnittstelle durch die Mitglieder des abstrahierten Clients ersetzen können.

Eine Implementierung von IHttpClientkann dann verwendet werden, um ein reales / konkretes HttpClientoder ein anderes Objekt für diese Angelegenheit zu verpacken / anzupassen , das verwendet werden kann, um HTTP-Anforderungen zu stellen, da das, was Sie wirklich wollten, ein Dienst war, der diese Funktionalität gemäß den HttpClientspezifischen Anforderungen bereitstellte . Die Verwendung der Abstraktion ist ein sauberer (meiner Meinung nach) und SOLID-Ansatz und kann Ihren Code wartbarer machen, wenn Sie den zugrunde liegenden Client gegen etwas anderes austauschen müssen, wenn sich das Framework ändert.

Hier ist ein Ausschnitt davon, wie eine Implementierung durchgeführt werden könnte.

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

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

    //...other code

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

    //...other code
}

Wie Sie im obigen Beispiel sehen können, ist ein Großteil des schweren Hebens, das normalerweise mit der Verwendung verbunden HttpClientist, hinter der Abstraktion verborgen.

Ihre Verbindungsklasse kann dann mit dem abstrahierten Client injiziert werden

public class Connection
{
    private IHttpClient _httpClient;

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

Ihr Test kann dann verspotten, was für Ihr SUT benötigt wird

private IHttpClient _httpClient;

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

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

    // Assuming doSomething uses the client to make
    // a request for a model of type SomeModelObject
    client.doSomething();  
}
Nkosi
quelle
Das ist die Antwort. Eine Abstraktion oben HttpClientund ein Adapter zum Erstellen Ihrer spezifischen Instanz mit HttpClientFactory. Dadurch wird das Testen der Logik über die HTTP-Anforderung hinaus trivial, was hier das Ziel ist.
Pimbrouwers
13

Aufbauend auf den anderen Antworten schlage ich diesen Code vor, der keine externen Abhängigkeiten aufweist:

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public async Task MyTestMethod()
    {
        var httpClient = new HttpClient(new MockHttpMessageHandler());

        var content = await httpClient.GetStringAsync("http://some.fake.url");

        Assert.AreEqual("Content as string", content);
    }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Content as string")
        };

        return await Task.FromResult(responseMessage);
    }
}
Pius
quelle
4
Sie testen effektiv Ihren Mock. Die wahre Stärke eines Mocks besteht darin, dass Sie in jedem Test Erwartungen setzen und sein Verhalten ändern können. Die Tatsache, dass Sie einige HttpMessageHandlerselbst implementieren müssen, macht dies nahezu unmöglich - und das müssen Sie, weil die Methoden es sind protected internal.
MarioDS
3
@MarioDS Ich denke, der Punkt ist, dass Sie die HTTP-Antwort verspotten können, um den Rest des Codes zu testen. Wenn Sie eine Factory injizieren, die den HttpClient erhält, können Sie diesen HttpClient in Tests bereitstellen.
chris31389
13

Ich denke, das Problem ist, dass Sie es nur ein wenig auf den Kopf gestellt haben.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

    public AuroraClient() : this(new HttpClientHandler())
    {
    }

    public AuroraClient(HttpMessageHandler messageHandler)
    {
        _client = new HttpClient(messageHandler);
    }
}

Wenn Sie sich die Klasse oben ansehen, denke ich, dass Sie das wollen. Microsoft empfiehlt, den Client für eine optimale Leistung am Leben zu halten, damit Sie diese Art von Struktur verwenden können. Auch der HttpMessageHandler ist eine abstrakte Klasse und daher verspottbar. Ihre Testmethode würde dann folgendermaßen aussehen:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var mockMessageHandler = new Mock<HttpMessageHandler>();
    // Set up your mock behavior here
    var auroraClient = new AuroraClient(mockMessageHandler.Object);
    // Act
    // Assert
}

Auf diese Weise können Sie Ihre Logik testen, während Sie sich über das Verhalten des HttpClient lustig machen.

Tut mir leid, nachdem ich dies geschrieben und selbst ausprobiert habe, wurde mir klar, dass Sie die geschützten Methoden auf dem HttpMessageHandler nicht verspotten können. Anschließend habe ich den folgenden Code hinzugefügt, um die Injektion eines richtigen Mocks zu ermöglichen.

public interface IMockHttpMessageHandler
{
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}

public class MockHttpMessageHandler : HttpMessageHandler
{
    private readonly IMockHttpMessageHandler _realMockHandler;

    public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
    {
        _realMockHandler = realMockHandler;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _realMockHandler.SendAsync(request, cancellationToken);
    }
}

Die damit geschriebenen Tests sehen dann ungefähr so ​​aus:

[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
    // Arrange
    var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
    // Set up Mock behavior here.
    var client = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
    // Act
    // Assert
}
Joshua Dooms
quelle
9

Einer meiner Kollegen bemerkte, dass die meisten HttpClientMethoden alle SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)unter der Haube aufgerufen werden. Dies ist eine virtuelle Methode aus HttpMessageInvoker:

Der bei weitem einfachste Weg, sich zu verspotten, HttpClientbestand darin, einfach diese bestimmte Methode zu verspotten:

var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);

und Ihr Code kann die meisten (aber nicht alle) HttpClientKlassenmethoden aufrufen , einschließlich einer regulären

httpClient.SendAsync(req)

Überprüfen Sie hier, um https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs zu bestätigen

Adam
quelle
1
Dies funktioniert jedoch nicht für Code, der SendAsync(HttpRequestMessage)direkt aufruft . Wenn Sie Ihren Code so ändern können, dass diese Komfortfunktion nicht verwendet wird, SendAsyncist es die sauberste Lösung, die ich gefunden habe, HttpClient direkt durch Überschreiben zu verspotten .
Dylan Nicholson
8

Eine Alternative wäre, einen Stub-HTTP-Server einzurichten, der vordefinierte Antworten basierend auf einem Muster zurückgibt, das mit der Anforderungs-URL übereinstimmt. Dies bedeutet, dass Sie echte HTTP-Anforderungen testen, die nicht verspottet werden. In der Vergangenheit hätte dies einen erheblichen Entwicklungsaufwand gekostet und wäre viel zu langsam gewesen, um für Unit-Tests in Betracht gezogen zu werden. Die OSS-Bibliothek WireMock.net ist jedoch einfach zu verwenden und schnell genug, um mit vielen Tests ausgeführt zu werden. Das Setup besteht aus einigen Codezeilen:

var server = FluentMockServer.Start();
server.Given(
      Request.Create()
      .WithPath("/some/thing").UsingGet()
   )
   .RespondWith(
       Response.Create()
       .WithStatusCode(200)
       .WithHeader("Content-Type", "application/json")
       .WithBody("{'attr':'value'}")
   );

Weitere Details und Anleitungen zur Verwendung von Wiremock in Tests finden Sie hier.

Alastairtree
quelle
8

Hier ist eine einfache Lösung, die für mich gut funktioniert hat.

Verwenden der Moq-Mocking-Bibliothek.

// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
   .Protected()
   // Setup the PROTECTED method to mock
   .Setup<Task<HttpResponseMessage>>(
      "SendAsync",
      ItExpr.IsAny<HttpRequestMessage>(),
      ItExpr.IsAny<CancellationToken>()
   )
   // prepare the expected response of the mocked http call
   .ReturnsAsync(new HttpResponseMessage()
   {
      StatusCode = HttpStatusCode.OK,
      Content = new StringContent("[{'id':1,'value':'1'}]"),
   })
   .Verifiable();

// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
   BaseAddress = new Uri("http://test.com/"),
};

var subjectUnderTest = new MyTestClass(httpClient);

// ACT
var result = await subjectUnderTest
   .GetSomethingRemoteAsync('api/test/whatever');

// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);

// also check the 'http' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");

handlerMock.Protected().Verify(
   "SendAsync",
   Times.Exactly(1), // we expected a single external request
   ItExpr.Is<HttpRequestMessage>(req =>
      req.Method == HttpMethod.Get  // we expected a GET request
      && req.RequestUri == expectedUri // to this uri
   ),
   ItExpr.IsAny<CancellationToken>()
);

Quelle: https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/

j7nn7k
quelle
Ich habe das auch erfolgreich genutzt. Ich ziehe es vor, in einer noch größeren Nuget-Abhängigkeit zu dfraggen, und Sie lernen tatsächlich ein bisschen mehr darüber, was auch unter der Haube passiert. Das Schöne ist, dass die meisten Methoden SendAsyncohnehin verwendet werden, sodass kein zusätzliches Setup erforderlich ist.
Steve Pettifer
4

Viele der Antworten überzeugen mich nicht.

Stellen Sie sich zunächst vor, Sie möchten eine Methode testen, die verwendet wird HttpClient. Sie sollten nicht HttpClientdirekt in Ihrer Implementierung instanziieren . Sie sollten einer Fabrik die Verantwortung übertragen, eine Instanz HttpClientfür Sie bereitzustellen. Auf diese Weise können Sie später in dieser Fabrik verspotten und zurückgeben, was immer HttpClientSie wollen (z. B. eine Verspottung HttpClientund nicht die echte).

Sie hätten also eine Fabrik wie die folgende:

public interface IHttpClientFactory
{
    HttpClient Create();
}

Und eine Implementierung:

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

Natürlich müssten Sie diese Implementierung in Ihrem IoC-Container registrieren. Wenn Sie Autofac verwenden, ist dies ungefähr so:

builder
    .RegisterType<IHttpClientFactory>()
    .As<HttpClientFactory>()
    .SingleInstance();

Jetzt hätten Sie eine ordnungsgemäße und überprüfbare Implementierung. Stellen Sie sich vor, Ihre Methode ist ungefähr so:

public class MyHttpClient
    : IMyHttpClient
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<string> PostAsync(Uri uri, string content)
    {
        using (var client = _httpClientFactory.Create())
        {
            var clientAddress = uri.GetLeftPart(UriPartial.Authority);
            client.BaseAddress = new Uri(clientAddress);
            var content = new StringContent(content, Encoding.UTF8, "application/json");
            var uriAbsolutePath = uri.AbsolutePath;
            var response = await client.PostAsync(uriAbsolutePath, content);
            var responseJson = response.Content.ReadAsStringAsync().Result;
            return responseJson;
        }
    }
}

Nun zum Testteil. HttpClienterstreckt sich HttpMessageHandler, was abstrakt ist. Lassen Sie uns einen "Mock" erstellen HttpMessageHandler, der einen Delegaten akzeptiert, sodass wir bei Verwendung des Mocks auch jedes Verhalten für jeden Test einrichten können.

public class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsyncFunc.Invoke(request, cancellationToken);
    }
}

Und jetzt und mit Hilfe von Moq (und FluentAssertions, einer Bibliothek, die Unit-Tests besser lesbar macht) haben wir alles, was Sie zum Unit-Test unserer verwendeten Methode PostAsync benötigen HttpClient

public static class PostAsyncTests
{
    public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
        : Given_WhenAsync_Then_Test
    {
        private SalesOrderHttpClient _sut;
        private Uri _uri;
        private string _content;
        private string _expectedResult;
        private string _result;

        protected override void Given()
        {
            _uri = new Uri("http://test.com/api/resources");
            _content = "{\"foo\": \"bar\"}";
            _expectedResult = "{\"result\": \"ok\"}";

            var httpClientFactoryMock = new Mock<IHttpClientFactory>();
            var messageHandlerMock =
                new MockHttpMessageHandler((request, cancellation) =>
                {
                    var responseMessage =
                        new HttpResponseMessage(HttpStatusCode.Created)
                        {
                            Content = new StringContent("{\"result\": \"ok\"}")
                        };

                    var result = Task.FromResult(responseMessage);
                    return result;
                });

            var httpClient = new HttpClient(messageHandlerMock);
            httpClientFactoryMock
                .Setup(x => x.Create())
                .Returns(httpClient);

            var httpClientFactory = httpClientFactoryMock.Object;

            _sut = new SalesOrderHttpClient(httpClientFactory);
        }

        protected override async Task WhenAsync()
        {
            _result = await _sut.PostAsync(_uri, _content);
        }


        [Fact]
        public void Then_It_Should_Return_A_Valid_JsonMessage()
        {
            _result.Should().BeEquivalentTo(_expectedResult);
        }
    }
}

Offensichtlich ist dieser Test albern und wir testen wirklich unseren Schein. Aber du kommst auf die Idee. Sie sollten eine aussagekräftige Logik in Abhängigkeit von Ihrer Implementierung testen, z.

  • Wenn der Codestatus der Antwort nicht 201 ist, sollte sie eine Ausnahme auslösen?
  • Was soll passieren, wenn der Antworttext nicht analysiert werden kann?
  • etc.

Der Zweck dieser Antwort war es, etwas zu testen, das HttpClient verwendet, und dies ist eine schöne, saubere Möglichkeit, dies zu tun.

diegosasw
quelle
3

Ich bin etwas spät auf der Party, aber ich benutze gerne Wiremocking ( https://github.com/WireMock-Net/WireMock.Net) ), wann immer dies möglich ist, beim Integrationstest eines Dotnet-Core-Mikroservices mit nachgeschalteten REST-Abhängigkeiten.

Durch die Implementierung einer TestHttpClientFactory, die die IHttpClientFactory erweitert, können wir die Methode überschreiben

HttpClient CreateClient (Stringname)

Wenn Sie also die genannten Clients in Ihrer App verwenden, haben Sie die Kontrolle über die Rückgabe eines HttpClient, der mit Ihrem Wiremock verbunden ist.

Das Gute an diesem Ansatz ist, dass Sie in der Anwendung, die Sie testen, nichts ändern und Kursintegrationstests ermöglichen, die eine tatsächliche REST-Anforderung an Ihren Service ausführen und den json (oder was auch immer) verspotten, den die tatsächliche nachgelagerte Anforderung zurückgeben soll. Dies führt zu präzisen Tests und so wenig Verspottung wie möglich in Ihrer Anwendung.

    public class TestHttpClientFactory : IHttpClientFactory 
{
    public HttpClient CreateClient(string name)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
            // G.Config is our singleton config access, so the endpoint 
            // to the running wiremock is used in the test
        };
        return httpClient;
    }
}

und

// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
Markus Foss
quelle
2

Da Sie HttpClientdie SendAsyncMethode verwenden, um alle auszuführen HTTP Requests, können Sie die override SendAsyncMethode verwenden und verspottenHttpClient .

Für diesen Wrap erstellen Sie HttpClientein interface, so etwas wie unten

public interface IServiceHelper
{
    HttpClient GetClient();
}

Verwenden Sie dann oben interfacefür die Abhängigkeitsinjektion in Ihrem Dienst, Beispiel unten

public class SampleService
{
    private readonly IServiceHelper serviceHelper;

    public SampleService(IServiceHelper serviceHelper)
    {
        this.serviceHelper = serviceHelper;
    }

    public async Task<HttpResponseMessage> Get(int dummyParam)
    {
        try
        {
            var dummyUrl = "http://www.dummyurl.com/api/controller/" + dummyParam;
            var client = serviceHelper.GetClient();
            HttpResponseMessage response = await client.GetAsync(dummyUrl);               

            return response;
        }
        catch (Exception)
        {
            // log.
            throw;
        }
    }
}

Erstellen Sie jetzt im Unit-Test-Projekt eine Hilfsklasse zum Verspotten SendAsync. Hier ist es eine FakeHttpResponseHandlerKlasse, die inheriting DelegatingHandlereine Option zum Überschreiben der SendAsyncMethode bietet . Nach dem Überschreiben der SendAsyncMethode Notwendigkeit , eine Antwort für jedes Setup , HTTP Requestdie ruft SendAsyncMethode, denn das ist ein schaffen Dictionarymit keyso Uriund valueso , HttpResponseMessageso dass , wann immer es liegt HTTP Requestund wenn die UriBegegnungen SendAsyncdes konfigurierte zurückkehren HttpResponseMessage.

public class FakeHttpResponseHandler : DelegatingHandler
{
    private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
    private readonly JavaScriptSerializer javaScriptSerializer;
    public FakeHttpResponseHandler()
    {
        fakeServiceResponse =  new Dictionary<Uri, HttpResponseMessage>();
        javaScriptSerializer =  new JavaScriptSerializer();
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
    {
        fakeServiceResponse.Remove(uri);
        fakeServiceResponse.Add(uri, httpResponseMessage);
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation having query string parameter.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    /// <param name="requestParameter">Query string parameter.</param>
    public void AddFakeServiceResponse<TQueryStringParameter>(Uri uri, HttpResponseMessage httpResponseMessage, TQueryStringParameter requestParameter)
    {
        var serilizedQueryStringParameter = javaScriptSerializer.Serialize(requestParameter);
        var actualUri = new Uri(string.Concat(uri, serilizedQueryStringParameter));
        fakeServiceResponse.Remove(actualUri);
        fakeServiceResponse.Add(actualUri, httpResponseMessage);
    }

    // all method in HttpClient call use SendAsync method internally so we are overriding that method here.
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if(fakeServiceResponse.ContainsKey(request.RequestUri))
        {
            return Task.FromResult(fakeServiceResponse[request.RequestUri]);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            RequestMessage = request,
            Content = new StringContent("Not matching fake found")
        });
    }
}

Erstellen Sie eine neue Implementierung, IServiceHelperindem Sie das Framework verspotten oder wie folgt. Diese FakeServiceHelperKlasse können wir verwenden, um die FakeHttpResponseHandlerKlasse zu injizieren , sodass sie immer dann verwendet wird , wenn die HttpClientvon classihr erstellte Klasse FakeHttpResponseHandler classanstelle der eigentlichen Implementierung verwendet wird.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

    public FakeServiceHelper(DelegatingHandler delegatingHandler)
    {
        this.delegatingHandler = delegatingHandler;
    }

    public HttpClient GetClient()
    {
        return new HttpClient(delegatingHandler);
    }
}

Und im Test konfigurieren Sie FakeHttpResponseHandler classdurch Hinzufügen von Uriund erwartet HttpResponseMessage. Das Urisollte der tatsächliche sein serviceEndpunkt , Uriso dass , wenn das overridden SendAsyncVerfahren von den tatsächlichen aufgerufen wird serviceImplementierung wird es , das Spiel Uriin Dictionaryund reagiert mit dem konfigurierte HttpResponseMessage. Nach der Konfiguration injizieren Sie die FakeHttpResponseHandler objectin die gefälschte IServiceHelperImplementierung. Fügen Sie FakeServiceHelper classdann den tatsächlichen Dienst ein, wodurch der tatsächliche Dienst die override SendAsyncMethode verwendet.

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

        var responseBody = await response.Content.ReadAsStringAsync();

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

GitHub Link: mit Beispielimplementierung

Ghosh-Arun
quelle
Während dieser Code die Frage lösen kann, einschließlich einer Erklärung, wie und warum dies das Problem löst, würde dies wirklich dazu beitragen, die Qualität Ihres Beitrags zu verbessern, und wahrscheinlich zu mehr Up-Votes führen. Denken Sie daran, dass Sie in Zukunft die Frage für die Leser beantworten, nicht nur für die Person, die jetzt fragt. Bitte bearbeiten Ihre Antwort, um Erklärungen hinzuzufügen und anzugeben, welche Einschränkungen und Annahmen gelten.
9огдан Опир
Vielen Dank an @ БогданОпир für das Feedback aktualisierte Erklärung.
Ghosh-Arun
1

Sie können die RichardSzalay MockHttp- Bibliothek verwenden, die den HttpMessageHandler verspottet und ein HttpClient-Objekt zurückgeben kann, das während der Tests verwendet werden soll.

GitHub MockHttp

PM> Installationspaket RichardSzalay.MockHttp

Aus der GitHub-Dokumentation

MockHttp definiert einen Ersatz-HttpMessageHandler, die Engine, die HttpClient steuert, die eine fließende Konfigurations-API und eine vordefinierte Antwort bereitstellt. Der Anrufer (z. B. die Serviceschicht Ihrer Anwendung) ist sich seiner Anwesenheit nicht bewusst.

Beispiel von GitHub

 var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

var json = await response.Content.ReadAsStringAsync();

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
Justin
quelle
1

Dies ist eine alte Frage, aber ich verspüre den Drang, die Antworten mit einer Lösung zu erweitern, die ich hier nicht gesehen habe.
Sie können die Microsoft-Assembly (System.Net.Http) fälschen und dann während des Tests ShinsContext verwenden.

  1. Klicken Sie in VS 2017 mit der rechten Maustaste auf die System.Net.Http-Assembly und wählen Sie "Fakes-Assembly hinzufügen".
  2. Fügen Sie Ihren Code in die Unit-Test-Methode unter ShimsContext.Create () ein. Auf diese Weise können Sie den Code isolieren, in dem Sie den HttpClient fälschen möchten.
  3. Abhängig von Ihrer Implementierung und Ihrem Test würde ich vorschlagen, alle gewünschten Aktionen zu implementieren, bei denen Sie eine Methode auf dem HttpClient aufrufen und den zurückgegebenen Wert fälschen möchten. Wenn Sie ShimHttpClient.AllInstances verwenden, wird Ihre Implementierung in allen Instanzen gefälscht, die während Ihres Tests erstellt wurden. Wenn Sie beispielsweise die GetAsync () -Methode fälschen möchten, gehen Sie wie folgt vor:

    [TestMethod]
    public void FakeHttpClient()
    {
        using (ShimsContext.Create())
        {
            System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) =>
            {
              //Return a service unavailable response
              var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
              var task = Task.FromResult(httpResponseMessage);
              return task;
            };
    
            //your implementation will use the fake method(s) automatically
            var client = new Connection(_httpClient);
            client.doSomething(); 
        }
    }
Luca
quelle
1

Ich habe etwas sehr Einfaches gemacht, da ich in einer DI-Umgebung war.

public class HttpHelper : IHttpHelper
{
    private ILogHelper _logHelper;

    public HttpHelper(ILogHelper logHelper)
    {
        _logHelper = logHelper;
    }

    public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
    {
        HttpResponseMessage response;
        using (var client = new HttpClient())
        {
            if (headers != null)
            {
                foreach (var h in headers)
                {
                    client.DefaultRequestHeaders.Add(h.Key, h.Value);
                }
            }
            response = await client.GetAsync(uri);
        }

        return response;
    }

    public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
    {
        ...

        rawResponse = await GetAsync(uri, headers);

        ...
    }

}

und der Schein ist:

    [TestInitialize]
    public void Initialize()
    {
       ...
        _httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
       ...
    }

    [TestMethod]
    public async Task SuccessStatusCode_WithAuthHeader()
    {
        ...

        _httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
            Task<HttpResponseMessage>.Factory.StartNew(() =>
            {
                return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(_testData))
                };
            })
        );
        var result = await _httpHelper.Object.GetAsync<TestDTO>(...);

        Assert.AreEqual(...);
    }
Jorge Aguilar
quelle
1

Sie benötigen lediglich eine Testversion der HttpMessageHandlerKlasse, die Sie an HttpClientctor übergeben. Der Hauptpunkt ist, dass Ihre Testklasse HttpMessageHandlereinen HttpRequestHandlerDelegaten hat, den die Anrufer festlegen und einfach so behandeln können, HttpRequestwie sie es möchten.

public class FakeHttpMessageHandler : HttpMessageHandler
    {
        public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
        (r, c) => 
            new HttpResponseMessage
            {
                ReasonPhrase = r.RequestUri.AbsoluteUri,
                StatusCode = HttpStatusCode.OK
            };


        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(HttpRequestHandler(request, cancellationToken));
        }
    }

Sie können eine Instanz dieser Klasse verwenden, um eine konkrete HttpClient-Instanz zu erstellen. Über den HttpRequestHandler-Delegaten haben Sie die volle Kontrolle über ausgehende http-Anforderungen von HttpClient.

Dogu Arslan
quelle
1

Inspiriert von der Antwort von PointZeroTwo , hier ein Beispiel mit NUnit und FakeItEasy .

SystemUnderTest In diesem Beispiel ist die Klasse, die Sie testen möchten - kein Beispielinhalt dafür angegeben, aber ich gehe davon aus, dass Sie das bereits haben!

[TestFixture]
public class HttpClientTests
{
    private ISystemUnderTest _systemUnderTest;
    private HttpMessageHandler _mockMessageHandler;

    [SetUp]
    public void Setup()
    {
        _mockMessageHandler = A.Fake<HttpMessageHandler>();
        var httpClient = new HttpClient(_mockMessageHandler);

        _systemUnderTest = new SystemUnderTest(httpClient);
    }

    [Test]
    public void HttpError()
    {
        // Arrange
        A.CallTo(_mockMessageHandler)
            .Where(x => x.Method.Name == "SendAsync")
            .WithReturnType<Task<HttpResponseMessage>>()
            .Returns(Task.FromResult(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.InternalServerError,
                Content = new StringContent("abcd")
            }));

        // Act
        var result = _systemUnderTest.DoSomething();

        // Assert
        // Assert.AreEqual(...);
    }
}
thinkOfaNumber
quelle
Was ist, wenn ich einen Parameter an die Methode übergeben möchte, der gegen "x.Method.Name" angegeben ist?
Shailesh
0

Möglicherweise muss in Ihrem aktuellen Projekt ein Code geändert werden, aber für neue Projekte sollten Sie unbedingt Flurl verwenden.

https://flurl.dev

Es handelt sich um eine HTTP-Clientbibliothek für .NET mit einer fließenden Schnittstelle, die speziell die Testbarkeit von Code ermöglicht, mit dem HTTP-Anforderungen gestellt werden.

Es gibt viele Codebeispiele auf der Website, aber kurz gesagt, Sie verwenden es so in Ihrem Code.

Fügen Sie die Verwendungen hinzu.

using Flurl;
using Flurl.Http;

Senden Sie eine Get-Anfrage und lesen Sie die Antwort.

public async Task SendGetRequest()
{
   var response = await "https://example.com".GetAsync();
   // ...
}

In den Unit-Tests fungiert Flurl als Mock, der so konfiguriert werden kann, dass er sich wie gewünscht verhält und auch die getätigten Anrufe überprüft.

using (var httpTest = new HttpTest())
{
   // Arrange
   httpTest.RespondWith("OK", 200);

   // Act
   await sut.SendGetRequest();

   // Assert
   httpTest.ShouldHaveCalled("https://example.com")
      .WithVerb(HttpMethod.Get);
}
Christian Kadluba
quelle
0

Nach sorgfältiger Suche fand ich den besten Ansatz, um dies zu erreichen.

    private HttpResponseMessage response;

    [SetUp]
    public void Setup()
    {
        var handlerMock = new Mock<HttpMessageHandler>();

        handlerMock
           .Protected()
           .Setup<Task<HttpResponseMessage>>(
              "SendAsync",
              ItExpr.IsAny<HttpRequestMessage>(),
              ItExpr.IsAny<CancellationToken>())
           // This line will let you to change the response in each test method
           .ReturnsAsync(() => response);

        _httpClient = new HttpClient(handlerMock.Object);

        yourClinet = new YourClient( _httpClient);
    }

Wie Sie bemerkt haben, habe ich Moq- und Moq.Protected-Pakete verwendet.

Amin Mohamed
quelle
0

Um meine 2 Cent hinzuzufügen. Um bestimmte http-Anforderungsmethoden zu verspotten, entweder Get oder Post. Das hat bei mir funktioniert.

mockHttpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(a => a.Method == HttpMethod.Get), ItExpr.IsAny<CancellationToken>())
                                                .Returns(Task.FromResult(new HttpResponseMessage()
                                                {
                                                    StatusCode = HttpStatusCode.OK,
                                                    Content = new StringContent(""),
                                                })).Verifiable();
Sam
quelle