it-swarm.com.de

Verspotteter HttpClient bei Unit-Tests

Ich habe Probleme mit dem Versuch, meinen Code für Unit-Tests zu verwenden. Das Problem ist das. Ich habe das Interface IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

Und die Klasse, die es verwendet, HttpHandler:

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

Und dann die Connection-Klasse, die die Clientimplementierung mit simpleIOC einfügt:

public class Connection
{
    private IHttpHandler _httpClient;

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

Und dann habe ich ein Unit-Testprojekt, das diese Klasse hat: 

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 habe ich natürlich Methoden in der Connection-Klasse, die Daten (JSON) von meinem Back-End abrufen. Ich möchte jedoch Unit-Tests für diese Klasse schreiben, und ich möchte natürlich keine Tests gegen das reale Backend schreiben, eher als ein verspottetes. Ich habe versucht, eine gute Antwort darauf ohne großen Erfolg zu googeln. Ich kann und habe Moq früher benutzt, aber nie etwas wie httpClient. Wie soll ich dieses Problem angehen?

Danke im Voraus.

71
tjugg

Ihr Interface zeigt die konkrete HttpClient-Klasse an. Daher sind alle Klassen, die dieses Interface verwenden, daran gebunden, dh, es kann nicht verspottet werden.

HttpClient erbt von keiner Schnittstelle, Sie müssen also Ihre eigene schreiben. Ich schlage ein Dekorateur-like 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 Ihre 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 HttpClientHandler seine eigene HttpClient erstellt. Sie können dann natürlich mehrere Klassen erstellen, die IHttpHandler auf verschiedene Arten implementieren.

Das Hauptproblem bei diesem Ansatz ist, dass Sie effektiv eine Klasse schreiben, die nur Methoden in einer anderen Klasse aufruft. Sie können jedoch eine Klasse erstellen, die erbt von HttpClient (siehe Beispiel von Nkosi ist ein viel besserer Ansatz als meiner. Das Leben wäre viel einfacher, wenn HttpClient eine Schnittstelle hätte, über die Sie sich lustig machen könnten, leider nicht.

Dieses Beispiel ist jedoch nicht das goldene Ticket. IHttpHandler stützt sich weiterhin auf HttpResponseMessage, das zu System.Net.Http-Namespace gehört. Wenn Sie also andere Implementierungen als HttpClient benötigen, müssen Sie eine Art Zuordnung durchführen, um deren Antworten in HttpResponseMessage-Objekte zu konvertieren. Dies ist natürlich nur ein Problem wenn Sie mehrere Implementierungen verwenden müssen von IHttpHandler, aber es sieht nicht so aus, als würden Sie es tun, also ist es nicht das Ende der Welt, aber es ist etwas, über das Sie nachdenken müssen.

Jedenfalls können Sie einfach IHttpHandler nachahmen, ohne sich um die konkrete HttpClient-Klasse sorgen zu müssen, da sie weggerissen wurde.

Ich empfehle das Testen der Nicht-Async-Methoden, da diese immer noch asynchrone Methoden aufrufen, ohne sich jedoch um asynchrone Unit-Tests kümmern zu müssen, siehe hier

24
Mike Eason

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

Wenn Sie lieber ein DSL als Moq verwenden möchten, habe ich eine Bibliothek auf GitHub/Nuget, die die Dinge ein wenig 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'}
165
Richard Szalay

Ich stimme mit einigen der anderen Antworten überein, dass der beste Ansatz darin besteht, HttpMessageHandler zu verspotten, anstatt HttpClient einzuwickeln. Diese Antwort ist insofern einzigartig, als HttpClient immer noch injiziert wird, wodurch es möglich ist, dass es sich um einen Singleton oder um einen mit Abhängigkeitseingabe verwalteten handelt.

"HttpClient soll einmalig 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);
        }
    }
}
28
PointZeroTwo

Dies ist eine häufig gestellte Frage, und ich war stark auf der Seite, die die Fähigkeit wollte, HttpClient zu verspotten, aber ich glaube, ich kam schließlich zu der Erkenntnis, dass Sie sich nicht über HttpClient lustig machen sollten. Das scheint logisch zu sein, aber ich denke, wir wurden von Dingen, die wir in Open-Source-Bibliotheken sehen, einer Gehirnwäsche unterzogen. 

Wir sehen oft "Clients", die wir im Code verspotten, um isoliert testen zu können. Daher versuchen wir automatisch, dasselbe Prinzip auf HttpClient anzuwenden. HttpClient macht eigentlich viel; Man kann sich das als Manager für HttpMessageHandler vorstellen, also will man das nicht verspotten, deshalb hat immer noch keine Schnittstelle. Der Teil, an dem Sie sich wirklich für Unit-Tests oder das Design Ihrer Services interessieren, ist der HttpMessageHandler, da dies die Antwort zurückgibt und Sie können das simulieren. 

Es lohnt sich auch darauf hinzuweisen, dass Sie HttpClient wahrscheinlich wie ein größeres Geschäft behandeln sollten. Zum Beispiel: Halten Sie die Installation neuer HttpClients auf ein Minimum. Sie können sie wiederverwenden. Sie sind so konzipiert, dass sie wiederverwendet werden können und verbrauchen weniger Ressourcen, wenn Sie dies tun. Wenn Sie anfangen, es wie ein größeres Geschäft zu behandeln, wird es sich viel mehr als falsch anfühlen, wenn Sie es verspotten möchten. Jetzt wird der Message-Handler die Sache, die Sie injizieren, und nicht der Client. 

Mit anderen Worten: Entwerfen Sie Ihre Abhängigkeiten um den Handler herum und nicht um den Client. Besser noch, abstrakte "Dienste", die HttpClient verwenden, mit denen Sie einen Handler einschleusen können, und dies stattdessen als injizierbare Abhängigkeit verwenden. In Ihren Tests können Sie dann den Handler fälschen, um die Antwort zum Einrichten der Tests zu steuern.

HttpClient zu verpacken ist eine verrückte Zeitverschwendung.

Update: Siehe das Beispiel von Joshua Dooms. Es ist genau das, was ich empfehle.

23
Sinaesthetic

Wie auch in den Kommentaren erwähnt, müssen Sie abstract die HttpClient entfernen, um nicht mit ihr gekoppelt zu werden. Ich habe in der Vergangenheit etwas Ähnliches gemacht. Ich werde versuchen, das, was ich getan habe, an das anzupassen, was Sie versuchen.

Sehen Sie sich zuerst die HttpClient-Klasse an und entscheiden Sie, welche Funktionalität sie bereitstellt. 

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 zu bestimmten Zwecken. Ich entfernte die meisten Abhängigkeiten von allem, was mit HttpClient zu tun hatte, und konzentrierte mich auf das, was ich zurückgeben wollte. Sie sollten prüfen, wie Sie die HttpClient abstrahieren möchten, um nur die erforderliche Funktionalität bereitzustellen.

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

Ich würde sogar empfehlen, auf IHttpHandler vollständig zu verzichten und die HttpClient-Abstraktion IHttpClient zu verwenden. Aber ich wähle einfach nicht aus, da Sie den Rumpf Ihrer Handler-Schnittstelle durch die Mitglieder des abstrahierten Clients ersetzen können.

Eine Implementierung der IHttpClient kann dann verwendet werden, um eine reale/konkrete HttpClient oder ein beliebiges anderes Objekt, das verwendet werden kann, um HTTP-Anforderungen so zu erstellen, dass sie einen Dienst bereitstellt, der die gewünschte Funktionalität für HttpClient bereitstellt. speziell. Die Verwendung der Abstraktion ist ein sauberer Ansatz (Meine Meinung) und SOLID und kann Ihren Code leichter pflegen, wenn Sie den zugrunde liegenden Client gegen etwas anderes austauschen müssen, wenn sich das Framework ändert.

Hier ist ein Ausschnitt, 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 der schweren Beanspruchung, die normalerweise mit HttpClient verbunden ist, hinter der Abstraktion verborgen. 

Ihre Verbindungsklasse kann dann mit dem abstrahierten Client eingefügt werden

public class Connection
{
    private IHttpClient _httpClient;

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

Ihr Test kann dann nachahmen, was für Ihr SUT erforderlich ist

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

Aufbauend auf den anderen Antworten empfehle ich diesen Code, der keine äußeren Abhängigkeiten hat:

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

Ich denke, das Problem ist, dass Sie es nur ein bisschen verkehrt herum 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 erhalten. Diese Art von Struktur ermöglicht Ihnen dies. Auch der HttpMessageHandler ist eine abstrakte Klasse und daher spottbar. Ihre Testmethode würde dann so 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 das Verhalten des HttpClient verspotten.

Sorry Jungs, nachdem ich das geschrieben und selbst ausprobiert habe, wurde mir klar, dass Sie die geschützten Methoden des HttpMessageHandler nicht nachahmen können. Anschließend habe ich den folgenden Code hinzugefügt, um die Injektion eines richtigen Modells 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 auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
        // Act
        // Assert
    }
9
Joshua Dooms

Einer meiner Kollegen bemerkte, dass die meisten HttpClient-Methoden SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) unter der Haube aufrufen, was eine virtuelle Methode aus HttpMessageInvoker ist:

Der einfachste Weg, um HttpClient zu verspotten, war, diese spezielle Methode einfach 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) Klassenmethoden der Klasse HttpClient aufrufen, einschließlich einer regulären

httpClient.SendAsync(req)

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

7
Adam

Eine Alternative wäre das Einrichten eines HTTP-Stub-Servers, der gespeicherte Antworten basierend auf einem Muster zurückgibt, das mit der Anforderungs-URL übereinstimmt. Das heißt, Sie testen echte HTTP-Anforderungen und keine Mocks. In der Vergangenheit hätte dies erhebliche Entwicklungsanstrengungen erfordert und wäre zu langsam gewesen, um für Komponententests in Betracht gezogen zu werden. OSS-Bibliothek WireMock.net ist jedoch einfach zu verwenden und schnell genug, um mit vielen Tests ausgeführt zu werden in Anbetracht. Das Setup besteht aus ein paar Zeilen Code:

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 Informationen und Anleitungen zur Verwendung von Wiremock in Tests finden Sie hier.

4
alastairtree

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

GitHub MockHttp

PM> Install-Package RichardSzalay.MockHttp

Aus der GitHub-Dokumentation

MockHttp definiert einen Ersatz-HttpMessageHandler, die Engine, die HttpClient steuert, die eine fließende Konfigurations-API bereitstellt und eine vorgegebene Antwort bereitstellt. Der Anrufer (z. B. die Service-Schicht Ihrer Anwendung) weiß nicht, ob er anwesend ist.

Beispiel aus 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'}
2
Justin

Der Party etwas später beitreten, aber ich mag es, wenn wir möglichst in dem Integrationstest eines Dotnet-Core-Microservice mit Downstream REST Wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) verwenden. Abhängigkeiten.

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

HttpClient CreateClient (Name der Zeichenfolge)

Wenn Sie also die benannten Clients in Ihrer App verwenden, haben Sie die Kontrolle, dass Sie einen HttpClient, der mit Ihrem Wiremock verbunden ist, zurückgeben können.

Das Gute an diesem Ansatz ist, dass Sie nichts in der von Ihnen getesteten Anwendung ändern und Kursintegrationstests ermöglichen, die eine tatsächliche REST -Anforderung an Ihren Dienst senden und den Json (oder was auch immer) die tatsächliche Downstream-Anforderung verspotten Rückkehr. Dies führt zu prägnanten Tests und so wenig 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);
2
Markus Foss

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 Spott 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(...);
    }
1
Jorge Aguilar

Sie benötigen lediglich eine Testversion der HttpMessageHandler-Klasse, die Sie an HttpClient ctor übergeben. Der wichtigste Punkt ist, dass die HttpMessageHandler-Testklasse über einen HttpRequestHandler-Delegaten verfügt, den die Anrufer festlegen und mit dem HttpRequest-Element einfach so verfahren können, wie 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 vollständige Kontrolle über ausgehende HTTP-Anforderungen von HttpClient.

1
Dogu Arslan

Viele Antworten überzeugen mich nicht.

Stellen Sie sich vor, Sie möchten eine Methode testen, die HttpClient verwendet. Sie sollten HttpClient nicht direkt in Ihrer Implementierung instanziieren. Sie sollten eine Factory mit der Verantwortung beauftragen, eine Instanz von HttpClient für Sie bereitzustellen. Auf diese Weise können Sie später eine Verspottung dieser Fabrik durchführen und das gewünschte HttpClient zurückgeben (z. B. eine Verspottung HttpClient und 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, würde es ungefähr so ​​aussehen:

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

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

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. HttpClient erweitert HttpMessageHandler, was abstrakt ist. Lassen Sie uns ein "Mock" von HttpMessageHandler erstellen, das einen Delegaten akzeptiert, damit wir, wenn wir das Mock verwenden, 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 haben wir mit Hilfe von Moq (und FluentAssertions, einer Bibliothek, die Unit-Tests lesbarer macht) alles, was zum Unit-Test unserer Methode PostAsync erforderlich ist, die HttpClient verwendet.

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 dumm und wir testen wirklich unseren Schein. Aber du kommst auf die Idee. Sie sollten je nach Implementierung eine aussagekräftige Logik testen, z.

  • wenn der Codestatus der Antwort nicht 201 ist, sollte eine Ausnahme ausgelöst werden?
  • was soll passieren, wenn der Antworttext nicht analysiert werden kann?
  • usw.

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

1
iberodev

Dies ist eine alte Frage, aber ich spü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 Assembly System.Net.Http und wählen Sie "Fakes Assembly hinzufügen".
  2. Fügen Sie Ihren Code in die Komponententestmethode unter ShimsContext.Create () ein. Auf diese Weise können Sie den Code isolieren, an 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. Die Verwendung von ShimHttpClient.AllInstances fälscht Ihre Implementierung in allen Instanzen, die während des Tests erstellt wurden. Wenn Sie beispielsweise die GetAsync () -Methode fälschen möchten, gehen Sie folgendermaßen 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(); 
        }
    }
    
1
Zeller

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

SystemUnderTest in diesem Beispiel ist die Klasse, die Sie testen möchten - kein Beispielinhalt dafür, 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(...);
    }
}
0
thinkOfaNumber

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

Verwenden der moq-Spottbibliothek.

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

0
j7nn7k