Mocking HttpClient nei test unitari


111

Ho alcuni problemi cercando di avvolgere il mio codice da utilizzare nei test unitari. Il problema è questo. Ho l'interfaccia IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

E la classe che lo utilizza, HttpHandler:

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

E poi la classe Connection, che utilizza simpleIOC per iniettare l'implementazione del client:

public class Connection
{
    private IHttpHandler _httpClient;

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

E poi ho un progetto di unit test che ha questa classe:

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?     

}

Ora ovviamente avrò metodi nella classe Connection che recupereranno i dati (JSON) da un mio back-end. Tuttavia, voglio scrivere test unitari per questa classe, e ovviamente non voglio scrivere test contro il back-end reale, piuttosto uno deriso. Ho provato a Google una buona risposta a questo senza grande successo. Posso e ho usato Moq per deridere prima, ma mai su qualcosa come httpClient. Come devo affrontare questo problema?

Grazie in anticipo.


1
Esporre un HttpClientnella tua interfaccia è dove si trova il problema. Stai costringendo il tuo cliente a usare la HttpClientclasse concreta. Invece, dovresti esporre un'astrazione del file HttpClient.
Mike Eason

Puoi spiegarlo un po 'più in profondità? Come devo costruire il costruttore delle classi di connessione perché non voglio che nessuna dipendenza di HttpClient in altre classi usi la classe Connection. Ad esempio, non voglio passare concerete HttpClient nel costruttore di Connection perché ciò renderebbe ogni altra classe che utilizza Connection dipendente da HttpClient?
tjugg

Per interesse, cosa hai cercato su Google? Apparentemente mockhttp potrebbe utilizzare alcuni miglioramenti SEO.
Richard Szalay

@ Mike - come menzionato nella mia risposta, non c'è davvero bisogno di astrarre HttpClient. È perfettamente testabile così com'è. Ho numerosi progetti che hanno suite di test senza backend che utilizzano questo metodo.
Richard Szalay

Risposte:


37

La tua interfaccia espone la HttpClientclasse concreta , quindi tutte le classi che usano questa interfaccia sono legate ad essa, questo significa che non può essere derisa.

HttpClientnon eredita da nessuna interfaccia quindi dovrai scriverne una tua. Suggerisco un motivo simile a un decoratore :

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

E la tua classe sarà simile a questa:

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

Il punto in tutto questo è che HttpClientHandlercrea il proprio HttpClient, quindi potresti ovviamente creare più classi che implementanoIHttpHandler in modi diversi.

Il problema principale con questo approccio è che stai effettivamente scrivendo una classe che chiama solo metodi in un'altra classe, tuttavia potresti creare una classe che eredita da HttpClient(vedi l'esempio di Nkosi , è un approccio molto migliore del mio). La vita sarebbe molto più semplice se HttpClientavessi un'interfaccia che potresti deridere, purtroppo non è così.

Tuttavia, questo esempio non è il biglietto d'oro. IHttpHandlerfa ancora affidamento su HttpResponseMessage, che appartiene allo System.Net.Httpspazio dei nomi, quindi se hai bisogno di altre implementazioni diverse da HttpClient, dovrai eseguire un qualche tipo di mappatura per convertire le loro risposte in HttpResponseMessageoggetti. Questo ovviamente è un problema solo se è necessario utilizzare più implementazioni diIHttpHandler ma non sembra che tu lo faccia quindi non è la fine del mondo, ma è qualcosa a cui pensare.

Ad ogni modo, puoi semplicemente IHttpHandlerprendere in giro senza doversi preoccupare del cementoHttpClient classe in quanto è stata astratta.

Consiglio di testare i metodi non asincroni , poiché questi chiamano ancora i metodi asincroni ma senza il fastidio di doversi preoccupare dei metodi asincroni di unit test, vedere qui


Questo in effetti risponde alla mia domanda. Anche la risposta di Nkosis è corretta, quindi non sono sicuro di quale dovrei accettare come risposta, ma seguirò questa. Grazie ad entrambi per lo sforzo
tjugg

@tjugg Felice di aiutare. Sentiti libero di votare le risposte se le trovi utili.
Nkosi

3
Vale la pena notare che la principale differenza tra questa risposta e quella di Nkosi è che si tratta di un'astrazione molto più sottile. Thin è probabilmente un bene per un oggetto umile
Ben Aaronson

228

L'estensibilità di HttpClient risiede nel HttpMessageHandlerpassaggio al costruttore. Il suo scopo è consentire implementazioni specifiche della piattaforma, ma puoi anche prenderlo in giro. Non è necessario creare un wrapper decoratore per HttpClient.

Se preferisci un DSL all'utilizzo di Moq, ho una libreria su GitHub / Nuget che rende le cose un po 'più facili: https://github.com/richardszalay/mockhttp

var mockHttp = new MockHttpMessageHandler();

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

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

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

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

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

1
Quindi dovrei semplicemente passare MockHttpMessageHandler come classe Messagehandler Httphandler? O come l'hai implementato nei tuoi progetti
tjugg

2
Ottima risposta e qualcosa che inizialmente non avrei saputo. Rende lavorare con HttpClient non così male.
Bealer

6
Per le persone che non vogliono occuparsi dell'iniezione del client ma vogliono comunque una facile testabilità, è banale da raggiungere. Basta sostituire var client = new HttpClient()con var client = ClientFactory()e impostare un campo internal static Func<HttpClient> ClientFactory = () => new HttpClient();ea livello di test è possibile riscrivere questo campo.
Chris Marisic

3
@ChrisMarisic stai suggerendo una forma di posizione di servizio per sostituire l'iniezione. La posizione del servizio è un noto anti-pattern, quindi è preferibile l'iniezione di imho.
MarioDS

2
@MarioDS e, a prescindere, non dovresti affatto iniettare un'istanza HttpClient . Se sei deciso a usare l'iniezione constructer per questo, dovresti iniettare un HttpClientFactorycome in Func<HttpClient>. Dato che vedo HttpClient come puramente un dettaglio di implementazione e non una dipendenza, userò la statica proprio come illustrato sopra. Sto benissimo con i test che manipolano gli interni. Se mi interessa il puro ismo, metterò in piedi server completi e testerò i percorsi del codice live. Usare qualsiasi tipo di finzione significa accettare l'approssimazione del comportamento non il comportamento reale.
Chris Marisic

39

Sono d'accordo con alcune delle altre risposte secondo cui l'approccio migliore consiste nel simulare HttpMessageHandler piuttosto che eseguire il wrapping di HttpClient. Questa risposta è unica in quanto inietta ancora HttpClient, consentendogli di essere un singleton o gestito con l'inserimento delle dipendenze.

"HttpClient deve essere istanziato una volta e riutilizzato per tutta la durata di un'applicazione." ( Fonte ).

Mocking HttpMessageHandler può essere un po 'complicato perché SendAsync è protetto. Ecco un esempio completo, utilizzando xunit e Moq.

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

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

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

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

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

        private readonly HttpClient _httpClient;
    }

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

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

            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}

1
Mi è davvero piaciuto. L' mockMessageHandler.Protected()era l'assassino. Grazie per questo esempio. Permette di scrivere il test senza modificare affatto la sorgente.
tyrion

1
Cordiali saluti, Moq 4.8 supporta la derisione fortemente tipizzata dei membri protetti - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay

2
Sembra fantastico. Anche Moq supporta ReturnsAsync quindi il codice sarebbe simile a.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord

Grazie @kord, l'ho aggiunto alla risposta
PointZeroTwo

3
C'è un modo per verificare che "SandAsync" sia stato chiamato con alcuni parametri? Ho provato a usare ... Protected (). Verify (...), ma sembra che non funzioni con i metodi asincroni.
Rroman

29

Questa è una domanda comune, ed ero fortemente dalla parte che voleva la possibilità di deridere HttpClient, ma penso di essere finalmente giunto alla conclusione che non dovresti prendere in giro HttpClient. Sembra logico farlo, ma penso che abbiamo subito il lavaggio del cervello da cose che vediamo nelle librerie open source.

Spesso vediamo là fuori "Client" che prendiamo in giro nel nostro codice in modo da poterli testare isolatamente, quindi proviamo automaticamente ad applicare lo stesso principio a HttpClient. HttpClient in realtà fa molto; si può pensare ad esso come un manager per HttpMessageHandler, in modo da non vuoi prendere in giro che, e questo è il motivo per cui ancora non dispone di un'interfaccia. La parte che siete veramente interessati a unità di test, o progettare i propri servizi, anche, è il HttpMessageHandler perché è ciò che restituisce la risposta, e si può prendere in giro così.

Vale anche la pena sottolineare che probabilmente dovresti iniziare a trattare HttpClient come un affare più grande. Ad esempio: ridurre al minimo le istanze di nuovi HttpClients. Riutilizzali, sono progettati per essere riutilizzati e usa una tonnellata di risorse in meno se lo fai. Se inizi a trattarlo come un affare più grande, ti sembrerà molto più sbagliato volerlo prendere in giro e ora il gestore dei messaggi inizierà a essere la cosa che stai iniettando, non il client.

In altre parole, progetta le tue dipendenze attorno al gestore invece che al client. Ancora meglio, "servizi" astratti che utilizzano HttpClient che ti consentono di iniettare un gestore e che invece lo usano come dipendenza iniettabile. Quindi nei tuoi test, puoi simulare il gestore per controllare la risposta per l'impostazione dei tuoi test.

Il wrapping di HttpClient è una folle perdita di tempo.

Aggiornamento: vedi l'esempio di Joshua Dooms. È esattamente quello che sto raccomandando.


17

Come accennato anche nei commenti, è necessario astrarre il fileHttpClient per non essere accoppiato ad esso. Ho fatto qualcosa di simile in passato. Cercherò di adattare quello che ho fatto con quello che stai cercando di fare.

Primo sguardo al HttpClient classe e decidi quale funzionalità ha fornito che sarebbe stata necessaria.

Ecco una possibilità:

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

Ancora una volta, come affermato prima, questo era per scopi particolari. Ho completamente astratto via la maggior parte delle dipendenze da tutto ciò che riguarda HttpCliente mi sono concentrato su ciò che volevo restituire. Dovresti valutare come vuoi astrarre il fileHttpClient per fornire solo le funzionalità necessarie che desideri.

Questo ora ti consentirà di deridere solo ciò che è necessario per essere testato.

Consiglierei anche di eliminare IHttpHandlercompletamente e di usare l' HttpClientastrazione IHttpClient. Ma non sto scegliendo perché puoi sostituire il corpo dell'interfaccia del tuo gestore con i membri del client astratto.

Un'implementazione di IHttpClientpuò quindi essere utilizzata per avvolgere / adattare un oggetto reale / concreto HttpCliento qualsiasi altro oggetto, che può essere utilizzato per effettuare richieste HTTP poiché ciò che si voleva veramente era un servizio che fornisse quella funzionalità come appostaHttpClient specificamente. L'uso dell'astrazione è un approccio pulito (La mia opinione) e SOLIDO e può rendere il tuo codice più manutenibile se devi cambiare il client sottostante per qualcos'altro quando il framework cambia.

Ecco uno snippet di come potrebbe essere eseguita un'implementazione.

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

Come puoi vedere nell'esempio sopra, gran parte del sollevamento pesante solitamente associato all'uso HttpClientè nascosto dietro l'astrazione.

La tua classe di connessione può quindi essere iniettata con il client astratto

public class Connection
{
    private IHttpClient _httpClient;

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

Il tuo test può quindi deridere ciò che è necessario per il tuo SUT

private IHttpClient _httpClient;

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

    _httpClient = httpClientMock.Object;

    var client = new Connection(_httpClient);

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

QUESTA è la risposta. Un'astrazione sopra HttpCliente un adattatore per creare la tua istanza specifica usando HttpClientFactory. Fare questo rende banale il test della logica oltre la richiesta HTTP, che è l'obiettivo qui.
pimbrouwers

13

Basandosi sulle altre risposte, suggerisco questo codice, che non ha dipendenze esterne:

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

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

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

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

        return await Task.FromResult(responseMessage);
    }
}

4
Stai effettivamente testando la tua simulazione. Il vero potere di una simulazione è che puoi impostare aspettative e modificare il suo comportamento in ogni test. Il fatto che tu debba implementarne alcuni HttpMessageHandlerda solo lo rende quasi impossibile - e devi farlo perché i metodi lo sono protected internal.
MarioDS

3
@MarioDS Penso che il punto sia che puoi deridere la risposta HTTP in modo da poter testare il resto del codice. Se si inserisce un factory che ottiene HttpClient, nei test è possibile fornire questo HttpClient.
chris31389

13

Penso che il problema sia che hai capito solo un po 'sottosopra.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Se guardi la classe sopra, penso che questo sia quello che vuoi. Microsoft consiglia di mantenere attivo il client per ottenere prestazioni ottimali, quindi questo tipo di struttura ti consente di farlo. Anche HttpMessageHandler è una classe astratta e quindi mockable. Il tuo metodo di prova sarebbe quindi simile a questo:

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

Ciò consente di testare la logica mentre si prende in giro il comportamento di HttpClient.

Scusate ragazzi, dopo aver scritto questo e provato io stesso, mi sono reso conto che non potete deridere i metodi protetti su HttpMessageHandler. Successivamente ho aggiunto il codice seguente per consentire l'iniezione di un mock corretto.

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

I test scritti con questo assomigliano quindi al seguente:

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

9

Uno dei miei colleghi ha notato che la maggior parte dei HttpClientmetodi chiama tutti SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)sotto il cofano, che è un metodo virtuale fuori da HttpMessageInvoker:

Quindi di gran lunga il modo più semplice per deridere HttpClientera semplicemente deridere quel particolare metodo:

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

e il tuo codice può chiamare la maggior parte (ma non tutti) i HttpClientmetodi della classe, incluso un normale

httpClient.SendAsync(req)

Controlla qui per confermare https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs


1
SendAsync(HttpRequestMessage)Tuttavia, questo non funziona per alcun codice che chiama direttamente. Se puoi modificare il tuo codice per non utilizzare questa comoda funzione, deridere HttpClient direttamente sovrascrivendo SendAsyncè in realtà la soluzione più pulita che ho trovato.
Dylan Nicholson

8

Un'alternativa sarebbe quella di configurare un server HTTP stub che restituisca risposte predefinite in base al pattern che corrisponde all'URL della richiesta, il che significa che si testano le richieste HTTP reali e non i mock. Storicamente questo avrebbe richiesto uno sforzo di sviluppo significativo e sarebbe stato molto lento da prendere in considerazione per i test unitari, tuttavia la libreria OSS WireMock.net è facile da usare e abbastanza veloce da essere eseguita con molti test, quindi potrebbe valere la pena considerarla. L'installazione è costituita da poche righe di codice:

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

Puoi trovare maggiori dettagli e indicazioni sull'utilizzo di wiremock nei test qui.


8

Ecco una soluzione semplice, che ha funzionato bene per me.

Utilizzando la libreria di derisione moq.

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

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

var subjectUnderTest = new MyTestClass(httpClient);

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

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

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

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

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


L'ho usato anche con successo. Preferisco questo al dfrag in un'altra dipendenza nuget e in realtà impari anche un po 'di più su ciò che sta accadendo sotto il cofano. La cosa bella è che la maggior parte dei metodi finisce per essere utilizzata SendAsynccomunque, quindi non è richiesta alcuna configurazione aggiuntiva.
Steve Pettifer

4

Non sono convinto da molte delle risposte.

Prima di tutto, immagina di voler testare un metodo che utilizza HttpClient. Non dovresti istanziare HttpClientdirettamente nella tua implementazione. Dovresti iniettare una fabbrica con la responsabilità di fornire un'istanza di HttpClientper te. In questo modo puoi prendere in giro quella fabbrica in seguito e restituire quello HttpClientche vuoi (es: un mock HttpCliente non quello vero).

Quindi, avresti una fabbrica come la seguente:

public interface IHttpClientFactory
{
    HttpClient Create();
}

E un'implementazione:

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

Ovviamente dovresti registrare questa implementazione nel tuo contenitore IoC. Se usi Autofac sarebbe qualcosa del tipo:

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

Ora avresti un'implementazione corretta e verificabile. Immagina che il tuo metodo sia qualcosa del tipo:

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

Ora la parte di test. HttpClientsi estende HttpMessageHandler, che è astratto. Creiamo un "mock" di HttpMessageHandlerche accetta un delegato in modo che quando usiamo il mock possiamo anche impostare ogni comportamento per ogni test.

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

E ora, e con l'aiuto di Moq (e FluentAssertions, una libreria che rende gli unit test più leggibili), abbiamo tutto il necessario per unit test il nostro metodo PostAsync che utilizza 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);
        }
    }
}

Ovviamente questo test è stupido e stiamo davvero testando la nostra simulazione. Ma hai l'idea. Dovresti testare una logica significativa a seconda della tua implementazione come ..

  • se lo stato del codice della risposta non è 201, dovrebbe generare un'eccezione?
  • se il testo della risposta non può essere analizzato, cosa dovrebbe accadere?
  • eccetera.

Lo scopo di questa risposta era testare qualcosa che usa HttpClient e questo è un modo semplice e pulito per farlo.


4

Mi unisco alla festa un po 'tardi, ma mi piace usare wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) quando possibile nel test di integrazione di un microservizio dotnet core con dipendenze REST downstream.

Implementando un TestHttpClientFactory che estende IHttpClientFactory possiamo sovrascrivere il metodo

HttpClient CreateClient (nome stringa)

Pertanto, quando usi i client denominati all'interno della tua app, hai il controllo della restituzione di un HttpClient cablato al tuo wiremock.

La cosa buona di questo approccio è che non stai modificando nulla all'interno dell'applicazione che stai testando e abilita i test di integrazione del corso che eseguono una richiesta REST effettiva al tuo servizio e deridono il json (o qualsiasi altra cosa) che la richiesta a valle effettiva dovrebbe restituire. Questo porta a test concisi e il minor numero possibile di derisioni nella tua applicazione.

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

e

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

2

Dato che HttpClientusa il SendAsyncmetodo per eseguire tutto HTTP Requests, puoi override SendAsyncmetodo e deridere il file HttpClient.

Per quell'involucro che crea HttpClienta interface, qualcosa come sotto

public interface IServiceHelper
{
    HttpClient GetClient();
}

Quindi usa sopra interfaceper l'inserimento delle dipendenze nel tuo servizio, esempio sotto

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

Ora nel progetto di unit test creare una classe helper per il mocking SendAsync. Ecco una FakeHttpResponseHandlerclasse che inheriting DelegatingHandlerfornirà un'opzione per sovrascrivere il SendAsyncmetodo. Dopo aver sovrascritto il SendAsyncmetodo è necessario impostare una risposta per ciascuno HTTP Requestche sta chiamando il SendAsyncmetodo, per questo creare un Dictionarycon keyas Urie valueas in HttpResponseMessagemodo che ogni volta che c'è un HTTP Requeste se le Uricorrispondenze SendAsyncrestituiranno il file configurato 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")
        });
    }
}

Crea una nuova implementazione per IServiceHelpermocking framework o come di seguito. Questa FakeServiceHelperclasse possiamo usare per iniettare la FakeHttpResponseHandlerclasse in modo che ogni volta che viene HttpClientcreata da questa classverrà utilizzata al FakeHttpResponseHandler classposto dell'attuale implementazione.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

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

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

E in prova configurare FakeHttpResponseHandler classaggiungendo Urie previsto HttpResponseMessage. Il Uridovrebbe essere l'attuale servicepunto finale Uriin modo che quando il overridden SendAsyncmetodo viene chiamato dalla effettiva serviceimplementazione che corrisponderà al Uriin Dictionarye rispondono con il configurato HttpResponseMessage. Dopo aver configurato, inserire il file FakeHttpResponseHandler objectnella falsa IServiceHelperimplementazione. Quindi inserire il FakeServiceHelper classnel servizio effettivo che renderà il servizio effettivo a utilizzare il override SendAsyncmetodo.

[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: che sta avendo un'implementazione di esempio


Sebbene questo codice possa risolvere la domanda, inclusa una spiegazione di come e perché questo risolve il problema aiuterebbe davvero a migliorare la qualità del tuo post e probabilmente si tradurrebbe in più voti positivi. Ricorda che stai rispondendo alla domanda per i lettori in futuro, non solo alla persona che chiede ora. Si prega di modificare la risposta per aggiungere spiegazioni e dare un'indicazione di ciò si applicano le limitazioni e le assunzioni.
Богдан Опир

Grazie @ БогданОпир per la spiegazione aggiornata del feedback.
ghosh-arun

1

È possibile utilizzare la libreria RichardSzalay MockHttp che prende in giro HttpMessageHandler e può restituire un oggetto HttpClient da utilizzare durante i test.

GitHub MockHttp

PM> Pacchetto di installazione RichardSzalay.MockHttp

Dalla documentazione di GitHub

MockHttp definisce un HttpMessageHandler sostitutivo, il motore che guida HttpClient, che fornisce un'API di configurazione fluida e fornisce una risposta predefinita. Il chiamante (ad es. Il livello di servizio dell'applicazione) rimane ignaro della sua presenza.

Esempio da GitHub

 var mockHttp = new MockHttpMessageHandler();

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

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

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

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

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

1

Questa è una vecchia domanda, ma sento il bisogno di estendere le risposte con una soluzione che non ho visto qui.
Puoi falsificare l'assemblaggio Microsoft (System.Net.Http) e quindi utilizzare ShinsContext durante il test.

  1. In VS 2017, fare clic con il pulsante destro del mouse sull'assembly System.Net.Http e scegliere "Add Fakes Assembly"
  2. Inserisci il tuo codice nel metodo di unit test sotto un ShimsContext.Create () usando. In questo modo, puoi isolare il codice in cui intendi falsificare HttpClient.
  3. Dipende dalla tua implementazione e test, ti suggerirei di implementare tutte le azioni desiderate in cui chiami un metodo su HttpClient e vuoi falsificare il valore restituito. L'utilizzo di ShimHttpClient.AllInstances falsificherà la tua implementazione in tutte le istanze create durante il test. Ad esempio, se desideri falsificare il metodo GetAsync (), procedi come segue:

    [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

Ho fatto qualcosa di molto semplice, dato che ero in un ambiente DI.

public class HttpHelper : IHttpHelper
{
    private ILogHelper _logHelper;

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

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

        return response;
    }

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

        rawResponse = await GetAsync(uri, headers);

        ...
    }

}

e la beffa è:

    [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

Tutto ciò di cui hai bisogno è una versione di prova della HttpMessageHandlerclasse che passi a HttpClientctor. Il punto principale è che la HttpMessageHandlerclasse di test avrà un HttpRequestHandlerdelegato che i chiamanti possono impostare e gestire semplicemente HttpRequestcome desiderano.

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

È possibile utilizzare un'istanza di questa classe per creare un'istanza HttpClient concreta. Tramite il delegato HttpRequestHandler si ha il controllo completo sulle richieste http in uscita da HttpClient.


1

Ispirato dalla risposta di PointZeroTwo , ecco un esempio che utilizza NUnit e FakeItEasy .

SystemUnderTest in questo esempio è la classe che vuoi testare - nessun contenuto di esempio dato per esso, ma presumo che tu lo abbia già!

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

cosa succede se voglio passare un parametro al metodo, menzionato contro "x.Method.Name" ..?
Shailesh

0

Forse ci sarebbe del codice da cambiare nel tuo progetto attuale, ma per i nuovi progetti dovresti assolutamente considerare l'utilizzo di Flurl.

https://flurl.dev

È una libreria client HTTP per .NET con un'interfaccia fluida che abilita specificamente la testabilità per il codice che la utilizza per effettuare richieste HTTP.

Ci sono molti esempi di codice sul sito web, ma in poche parole lo usi in questo modo nel tuo codice.

Aggiungi gli usi.

using Flurl;
using Flurl.Http;

Invia una richiesta get e leggi la risposta.

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

Negli unit test Flurl agisce come un mock che può essere configurato per comportarsi come desiderato e anche per verificare le chiamate che sono state fatte.

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

   // Act
   await sut.SendGetRequest();

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

0

Dopo un'attenta ricerca, ho trovato l'approccio migliore per farlo.

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

Come hai notato ho usato i pacchetti Moq e Moq.Protected.


0

Per aggiungere i miei 2 centesimi. Per simulare metodi di richiesta http specifici Get o Post. Questo ha funzionato per me.

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();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.