Mocking HttpClient dans les tests unitaires

111

J'ai quelques problèmes en essayant d'encapsuler mon code pour être utilisé dans les tests unitaires. Le problème est le suivant. J'ai l'interface IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

Et la classe qui l'utilise, HttpHandler:

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

Et puis la classe Connection, qui utilise simpleIOC pour injecter l'implémentation client:

public class Connection
{
    private IHttpHandler _httpClient;

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

Et puis j'ai un projet de test unitaire qui a cette 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?     

}

Maintenant, évidemment, j'aurai des méthodes dans la classe Connection qui récupéreront des données (JSON) depuis mon back-end. Cependant, je veux écrire des tests unitaires pour cette classe, et évidemment je ne veux pas écrire de tests contre le vrai back-end, plutôt un simulé. J'ai essayé de google une bonne réponse à cela sans grand succès. Je peux et ai utilisé Moq pour me moquer avant, mais jamais sur quelque chose comme httpClient. Comment dois-je aborder ce problème?

Merci d'avance.

tjugg
la source
1
Exposer un HttpClientdans votre interface est là où se situe le problème. Vous obligez votre client à utiliser la HttpClientclasse concrète. Au lieu de cela, vous devez exposer une abstraction du HttpClient.
Mike Eason
Pouvez-vous l'expliquer un peu plus en profondeur? Comment dois-je construire le constructeur de classes de connexion car je ne veux pas de dépendances de HttpClient dans d'autres classes qui utilisent la classe Connection. Par exemple, je ne veux pas passer concerete HttpClient dans le constructeur de Connection car cela rendrait toute autre classe qui utilise Connection dépendante de HttpClient?
tjugg
Par intérêt, qu'avez-vous recherché sur Google? Apparemment, mockhttp pourrait utiliser des améliorations SEO.
Richard Szalay
@Mike - comme mentionné dans ma réponse, il n'est vraiment pas nécessaire d'abstraire HttpClient. Il est parfaitement testable tel quel. J'ai de nombreux projets qui ont des suites de tests sans backend utilisant cette méthode.
Richard Szalay

Réponses:

37

Votre interface expose la HttpClientclasse concrète , donc toutes les classes qui utilisent cette interface y sont liées, cela signifie qu'elle ne peut pas être moquée.

HttpClientn'hérite d'aucune interface, vous devrez donc écrire la vôtre. Je suggère un motif à la décoration:

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

Et votre classe ressemblera à ceci:

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

Le point dans tout cela est que cela HttpClientHandlercrée le sien HttpClient, vous pouvez bien sûr créer plusieurs classes qui implémentent IHttpHandlerde différentes manières.

Le principal problème avec cette approche est que vous écrivez effectivement une classe qui appelle simplement des méthodes dans une autre classe, mais vous pouvez créer une classe qui hérite de HttpClient(voir l'exemple de Nkosi , c'est une bien meilleure approche que la mienne). La vie serait beaucoup plus facile si vous HttpClientaviez une interface dont vous pourriez vous moquer, malheureusement ce n'est pas le cas.

Cet exemple n'est cependant pas le ticket d'or. IHttpHandlerrepose toujours sur HttpResponseMessage, qui appartient à l' System.Net.Httpespace de noms, donc si vous avez besoin d'autres implémentations que HttpClient, vous devrez effectuer une sorte de mappage pour convertir leurs réponses en HttpResponseMessageobjets. Ce n'est bien sûr un problème que si vous avez besoin d'utiliser plusieurs implémentations de IHttpHandlermais il ne semble pas que vous le fassiez, donc ce n'est pas la fin du monde, mais c'est quelque chose à penser.

Quoi qu'il en soit, vous pouvez simplement vous moquer IHttpHandlersans avoir à vous soucier de la HttpClientclasse concrète telle qu'elle a été abstraite.

Je recommande de tester les méthodes non asynchrones , car elles appellent toujours les méthodes asynchrones mais sans avoir à vous soucier des méthodes asynchrones de test unitaire, voir ici

Mike Eason
la source
Cela répond effectivement à ma question. La réponse de Nkosis est également correcte, donc je ne suis pas sûr que je devrais accepter comme réponse, mais je vais aller avec celle-ci. Merci à tous les deux pour l'effort
tjugg
@tjugg Heureux de vous aider. N'hésitez pas à voter les réponses si vous les avez trouvées utiles.
Nkosi le
3
Il convient de noter que la principale différence entre cette réponse et celle de Nkosi est qu'il s'agit d'une abstraction beaucoup plus fine. Mince est probablement bon pour un objet humble
Ben Aaronson
228

L'extensibilité de HttpClient réside dans le HttpMessageHandlerpassé au constructeur. Son objectif est d'autoriser des implémentations spécifiques à la plate-forme, mais vous pouvez également vous en moquer. Il n'est pas nécessaire de créer un wrapper décorateur pour HttpClient.

Si vous préférez un DSL à l'utilisation de Moq, j'ai une bibliothèque sur GitHub / Nuget qui facilite un peu les choses: 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
la source
1
Donc, je passerais simplement MockHttpMessageHandler comme classe Httphandler messagehandler? Ou comment l'avez-vous mis en œuvre dans vos propres projets
tjugg
2
Excellente réponse et quelque chose que je n'aurais pas su au départ. Rend le travail avec HttpClient pas si mal.
Bealer
6
Pour les personnes qui ne veulent pas se préoccuper de l'injection du client, mais qui souhaitent tout de même une testabilité facile, c'est trivial à réaliser. Il suffit de remplacer var client = new HttpClient()avec var client = ClientFactory()et configurer un champ internal static Func<HttpClient> ClientFactory = () => new HttpClient();et au niveau de test , vous pouvez réécrire ce domaine.
Chris Marisic
3
@ChrisMarisic vous suggérez une forme d'emplacement de service pour remplacer l'injection. L'emplacement du service est un anti-schéma bien connu, donc l'injection à mon humble avis est préférable.
MarioDS
2
@MarioDS et peu importe, vous ne devriez pas du tout injecter d' instance HttpClient . Si vous êtes déterminé à utiliser l'injection de constructeur pour cela, vous devriez injecter un HttpClientFactorycomme dans Func<HttpClient>. Étant donné que je considère HttpClient comme un simple détail d'implémentation et non comme une dépendance, j'utiliserai la statique comme je l'ai illustré ci-dessus. Je suis tout à fait d'accord avec les tests manipulant les internes. Si je me soucie du pur-isme, je vais mettre en place des serveurs complets et tester les chemins de code en direct. L'utilisation de tout type de simulation signifie que vous acceptez l'approximation du comportement et non le comportement réel.
Chris Marisic
39

Je suis d'accord avec certaines des autres réponses selon lesquelles la meilleure approche consiste à se moquer de HttpMessageHandler plutôt que d'envelopper HttpClient. Cette réponse est unique en ce qu'elle injecte toujours HttpClient, ce qui lui permet d'être un singleton ou géré avec une injection de dépendance.

"HttpClient est destiné à être instancié une fois et réutilisé tout au long de la vie d'une application." ( Source ).

Mocking HttpMessageHandler peut être un peu délicat car SendAsync est protégé. Voici un exemple complet, utilisant xunit et 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
la source
1
J'ai vraiment aimé ça. Le mockMessageHandler.Protected()était le tueur. Merci pour cet exemple. Cela permet d'écrire le test sans modifier du tout la source.
tyrion
1
FYI, Moq 4.8 prend en charge la moquerie fortement typée des membres protégés - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay
2
Cela a l'air génial. Moq prend également en charge ReturnsAsync pour que le code ressemble à.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord
Merci @kord, j'ai ajouté cela à la réponse
PointZeroTwo
3
Existe-t-il un moyen de vérifier que "SandAsync" a été appelé avec certains paramètres? J'ai essayé d'utiliser ... Protected (). Verify (...), mais il semble que cela ne fonctionne pas avec les méthodes asynchrones.
Rroman
29

C'est une question courante, et j'étais fortement du côté voulant la possibilité de se moquer de HttpClient, mais je pense que j'ai finalement réalisé que vous ne devriez pas vous moquer de HttpClient. Cela semble logique de le faire, mais je pense que nous avons subi un lavage de cerveau par des choses que nous voyons dans les bibliothèques open source.

Nous voyons souvent des «clients» que nous nous moquons dans notre code afin de pouvoir tester de manière isolée, nous essayons donc automatiquement d'appliquer le même principe à HttpClient. HttpClient fait beaucoup; vous pouvez le considérer comme un gestionnaire pour HttpMessageHandler, donc vous ne voulez pas vous moquer de cela, et c'est pourquoi il n'a toujours pas d'interface. La partie qui vous intéresse vraiment pour les tests unitaires, ou même la conception de vos services, est le HttpMessageHandler, car c'est ce qui renvoie la réponse, et vous pouvez vous en moquer.

Il convient également de souligner que vous devriez probablement commencer à traiter HttpClient comme une affaire plus importante. Par exemple: réduisez au minimum votre installation de nouveaux HttpClients. Réutilisez-les, ils sont conçus pour être réutilisés et utiliser une tonne de ressources en moins si vous le faites. Si vous commencez à le traiter comme une affaire plus importante, vous vous sentirez beaucoup plus mal de vouloir vous en moquer et maintenant le gestionnaire de messages commencera à être la chose que vous injectez, pas le client.

En d'autres termes, concevez vos dépendances autour du gestionnaire plutôt que du client. Mieux encore, des "services" abstraits qui utilisent HttpClient qui vous permettent d'injecter un gestionnaire et de l'utiliser comme dépendance injectable à la place. Ensuite, dans vos tests, vous pouvez simuler le gestionnaire pour contrôler la réponse pour la configuration de vos tests.

Wrapping HttpClient est une perte de temps insensée.

Mise à jour: voir l'exemple de Joshua Dooms. C'est exactement ce que je recommande.

Sinaesthésique
la source
17

Comme il est mentionné également dans les commentaires que vous devez abstraite loin la HttpClientfaçon de ne pas être couplé à elle. J'ai fait quelque chose de similaire dans le passé. J'essaierai d'adapter ce que j'ai fait à ce que vous essayez de faire.

Examinez d'abord la HttpClientclasse et décidez des fonctionnalités qu'elle fournissait qui seraient nécessaires.

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

Encore une fois, comme indiqué précédemment, c'était à des fins particulières. J'ai complètement fait abstraction de la plupart des dépendances à tout ce qui concerne HttpClientet me suis concentré sur ce que je voulais retourner. Vous devez évaluer la façon dont vous souhaitez abstraire le HttpClientpour fournir uniquement les fonctionnalités nécessaires que vous souhaitez.

Cela vous permettra désormais de vous moquer uniquement de ce qui doit être testé.

Je recommanderais même de supprimer IHttpHandlercomplètement et d'utiliser l' HttpClientabstraction IHttpClient. Mais je ne choisis tout simplement pas car vous pouvez remplacer le corps de votre interface de gestionnaire par les membres du client abstrait.

Une implémentation de IHttpClientpeut ensuite être utilisée pour envelopper / adapter un objet réel / concret HttpClientou tout autre objet, qui peut être utilisé pour faire des requêtes HTTP, car ce que vous vouliez vraiment était un service qui fournissait cette fonctionnalité comme étant HttpClientspécifiquement associé . L'utilisation de l'abstraction est une approche propre (à mon avis) et SOLIDE et peut rendre votre code plus maintenable si vous avez besoin de changer le client sous-jacent pour autre chose à mesure que le cadre change.

Voici un extrait de la façon dont une mise en œuvre pourrait être effectuée.

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

Comme vous pouvez le voir dans l'exemple ci-dessus, une grande partie du travail lourd généralement associé à l'utilisation HttpClientest cachée derrière l'abstraction.

Votre classe de connexion peut ensuite être injectée avec le client abstrait

public class Connection
{
    private IHttpClient _httpClient;

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

Votre test peut alors simuler ce qui est nécessaire pour votre 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();  
}
Nkosi
la source
CECI est la réponse. Une abstraction ci HttpClient- dessus et un adaptateur pour créer votre instance spécifique à l'aide de HttpClientFactory. Cela rend le test de la logique au-delà de la requête HTTP trivial, ce qui est le but ici.
pimbrouwers
13

En me basant sur les autres réponses, je suggère ce code, qui n'a pas de dépendances extérieures:

[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);
    }
}
pie
la source
4
Vous testez efficacement votre simulation. Le vrai pouvoir d'une simulation est que vous pouvez définir des attentes et modifier son comportement à chaque test. Le fait que vous deviez en implémenter HttpMessageHandlervous-même rend cela presque impossible - et vous devez le faire car les méthodes le sont protected internal.
MarioDS
3
@MarioDS Je pense que le fait est que vous pouvez vous moquer de la réponse HTTP afin de pouvoir tester le reste du code. Si vous injectez une fabrique qui obtient le HttpClient, dans les tests, vous pouvez fournir ce HttpClient.
chris31389
13

Je pense que le problème est que vous l'avez juste un peu à l'envers.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Si vous regardez la classe ci-dessus, je pense que c'est ce que vous voulez. Microsoft recommande de garder le client en vie pour des performances optimales, ce type de structure vous permet donc de le faire. Le HttpMessageHandler est également une classe abstraite et donc mockable. Votre méthode de test ressemblerait alors à ceci:

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

Cela vous permet de tester votre logique tout en se moquant du comportement du HttpClient.

Désolé les gars, après avoir écrit ceci et essayé moi-même, j'ai réalisé que vous ne pouvez pas vous moquer des méthodes protégées sur HttpMessageHandler. J'ai ensuite ajouté le code suivant pour permettre l'injection d'une maquette appropriée.

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

Les tests écrits avec ceci ressemblent alors à ceci:

[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
la source
9

Un de mes collègues a remarqué que la plupart des HttpClientméthodes appellent toutes SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)sous le capot, qui est une méthode virtuelle hors de HttpMessageInvoker:

Donc, de loin, le moyen le plus simple de se moquer HttpClientétait de simplement se moquer de cette méthode particulière:

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

et votre code peut appeler la plupart (mais pas la totalité) des HttpClientméthodes de classe, y compris un

httpClient.SendAsync(req)

Vérifiez ici pour confirmer https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

Adam
la source
1
Cela ne fonctionne pour aucun code qui appelle SendAsync(HttpRequestMessage)directement. Si vous pouvez modifier votre code pour ne pas utiliser cette fonction pratique, se moquer directement de HttpClient en remplaçant SendAsyncest en fait la solution la plus propre que j'ai trouvée.
Dylan Nicholson
8

Une alternative serait de configurer un serveur HTTP stub qui renvoie des réponses prédéfinies en fonction du modèle correspondant à l'URL de la demande, ce qui signifie que vous testez de vraies requêtes HTTP et non des simulations. Historiquement, cela aurait demandé des efforts de développement importants et aurait été bien trop lent à être pris en compte pour les tests unitaires, mais la bibliothèque OSS WireMock.net est facile à utiliser et suffisamment rapide pour être exécutée avec de nombreux tests. La configuration consiste en quelques lignes de 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'}")
   );

Vous pouvez trouver plus de détails et des conseils sur l'utilisation de wiremock dans les tests ici.

alastairtree
la source
8

Voici une solution simple, qui a bien fonctionné pour moi.

Utilisation de la bibliothèque moqueuse 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>()
);

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

j7nn7k
la source
J'ai également utilisé cela avec succès. Je préfère cela à la défragmentation dans une dépendance encore plus nuget et vous en apprenez un peu plus sur ce qui se passe sous le capot. Ce qui est bien, c'est que la plupart des méthodes finissent par être utilisées de SendAsynctoute façon, donc aucune configuration supplémentaire n'est requise.
Steve Pettifer du
4

Je ne suis pas convaincu par la plupart des réponses.

Tout d'abord, imaginez que vous souhaitez tester unitaire une méthode qui utilise HttpClient. Vous ne devez pas instancier HttpClientdirectement dans votre implémentation. Vous devez injecter une usine avec la responsabilité de vous fournir une instance de HttpClient. De cette façon, vous pouvez vous moquer plus tard de cette usine et retourner celle que HttpClientvous voulez (par exemple: une maquette HttpClientet pas la vraie).

Donc, vous auriez une usine comme celle-ci:

public interface IHttpClientFactory
{
    HttpClient Create();
}

Et une mise en œuvre:

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

Bien sûr, vous devrez enregistrer cette implémentation dans votre conteneur IoC. Si vous utilisez Autofac, ce serait quelque chose comme:

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

Maintenant, vous auriez une implémentation correcte et testable. Imaginez que votre méthode ressemble à quelque chose comme:

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

Maintenant, la partie test. HttpClients'étend HttpMessageHandler, ce qui est abstrait. Créons un "simulacre" de HttpMessageHandlerqui accepte un délégué de sorte que lorsque nous utilisons le simulacre, nous pouvons également configurer chaque comportement pour chaque 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);
    }
}

Et maintenant, et avec l'aide de Moq (et FluentAssertions, une bibliothèque qui rend les tests unitaires plus lisibles), nous avons tout ce qu'il faut pour tester unitaire notre méthode PostAsync qui utilise 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);
        }
    }
}

De toute évidence, ce test est idiot, et nous testons vraiment notre simulation. Mais vous voyez l'idée. Vous devez tester une logique significative en fonction de votre implémentation, telle que ..

  • si le statut de code de la réponse n'est pas 201, doit-il lever une exception?
  • si le texte de la réponse ne peut pas être analysé, que doit-il se passer?
  • etc.

Le but de cette réponse était de tester quelque chose qui utilise HttpClient et c'est une belle façon propre de le faire.

diegosasw
la source
4

Rejoindre la fête un peu tard, mais j'aime utiliser wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) chaque fois que possible dans le test d'intégration d'un microservice de base dotnet avec des dépendances REST en aval.

En implémentant un TestHttpClientFactory étendant le IHttpClientFactory, nous pouvons remplacer la méthode

HttpClient CreateClient (nom de chaîne)

Ainsi, lorsque vous utilisez les clients nommés dans votre application, vous contrôlez le retour d'un HttpClient câblé à votre wiremock.

La bonne chose à propos de cette approche est que vous ne changez rien dans l'application que vous testez, et permet aux tests d'intégration de cours de faire une requête REST réelle à votre service et de se moquer du json (ou autre) que la requête en aval réelle devrait renvoyer. Cela conduit à des tests concis et aussi peu de moquerie que possible dans votre application.

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

et

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

Depuis HttpClientutiliser la SendAsyncméthode pour tout exécuter HTTP Requests, vous pouvez utiliser une méthode et vous override SendAsyncmoquer du fichier HttpClient.

Pour cette enveloppe créant HttpClientun interface, quelque chose comme ci-dessous

public interface IServiceHelper
{
    HttpClient GetClient();
}

Ensuite, utilisez ci-dessus interfacepour l'injection de dépendances dans votre service, exemple ci-dessous

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

Maintenant, dans le projet de test unitaire, créez une classe d'assistance pour les moqueries SendAsync. Ici, c'est une FakeHttpResponseHandlerclasse qui inheriting DelegatingHandlerfournira une option pour remplacer la SendAsyncméthode. Après avoir remplacé la SendAsyncméthode, vous devez configurer une réponse pour chaque méthode HTTP Requestappelante SendAsync, pour cela, créez un Dictionaryavec au keyfur Uriet valueà HttpResponseMessagemesure que chaque fois qu'il y a un HTTP Requestet si les Uricorrespondances SendAsyncrenverront le fichier configuré 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")
        });
    }
}

Créez une nouvelle implémentation pour IServiceHelperen se moquant du framework ou comme ci-dessous. Cette FakeServiceHelperclasse, nous pouvons utiliser pour injecter la FakeHttpResponseHandlerclasse afin que chaque fois que le HttpClientcréé par cela, classil l'utilise à la FakeHttpResponseHandler classplace de l'implémentation réelle.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

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

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

Et en test, configurez FakeHttpResponseHandler classen ajoutant le Uriet attendu HttpResponseMessage. Le Uridoit être le servicepoint de terminaison réel de Urisorte que lorsque la overridden SendAsyncméthode est appelée à partir de l' serviceimplémentation réelle, elle correspond au Uriin Dictionaryet répond avec le configuré HttpResponseMessage. Après avoir configuré, injectez le FakeHttpResponseHandler objectdans la fausse IServiceHelperimplémentation. Puis injectez le FakeServiceHelper classdans le service réel qui permettra au service réel d'utiliser la override SendAsyncméthode.

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

Lien GitHub: qui a un exemple d'implémentation

ghosh-arun
la source
Bien que ce code puisse résoudre la question, inclure une explication sur comment et pourquoi cela résout le problème aiderait vraiment à améliorer la qualité de votre message et entraînerait probablement plus de votes à la hausse. N'oubliez pas que vous répondez à la question des lecteurs à l'avenir, pas seulement à la personne qui la pose maintenant. Veuillez modifier votre réponse pour ajouter des explications et donner une indication des limites et des hypothèses applicables.
Богдан Опир
Merci @ БогданОпир pour les commentaires mis à jour l'explication.
ghosh-arun
1

Vous pouvez utiliser la bibliothèque RichardSzalay MockHttp qui se moque de HttpMessageHandler et peut renvoyer un objet HttpClient à utiliser pendant les tests.

GitHub MockHttp

PM> Package d'installation RichardSzalay.MockHttp

À partir de la documentation GitHub

MockHttp définit un HttpMessageHandler de remplacement, le moteur qui pilote HttpClient, qui fournit une API de configuration fluide et fournit une réponse standardisée. L'appelant (par exemple, la couche de service de votre application) ne se rend pas compte de sa présence.

Exemple de 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
la source
1

C'est une vieille question, mais je ressens le besoin d'étendre les réponses avec une solution que je n'ai pas vue ici.
Vous pouvez simuler l'assemly Microsoft (System.Net.Http), puis utiliser ShinsContext pendant le test.

  1. Dans VS 2017, cliquez avec le bouton droit sur l'assembly System.Net.Http et choisissez "Add Fakes Assembly"
  2. Mettez votre code dans la méthode de test unitaire sous un ShimsContext.Create () en utilisant. De cette façon, vous pouvez isoler le code là où vous prévoyez de simuler HttpClient.
  3. Dépend de votre implémentation et de votre test, je suggérerais de mettre en œuvre toutes les actions souhaitées lorsque vous appelez une méthode sur HttpClient et que vous souhaitez simuler la valeur retournée. L'utilisation de ShimHttpClient.AllInstances simulera votre implémentation dans toutes les instances créées lors de votre test. Par exemple, si vous souhaitez simuler la méthode GetAsync (), procédez comme suit:

    [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
la source
1

J'ai fait quelque chose de très simple, car j'étais dans un environnement 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);

        ...
    }

}

et le simulacre est:

    [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
la source
1

Tout ce dont vous avez besoin est une version de test de la HttpMessageHandlerclasse que vous passez à HttpClientctor. Le point principal est que votre HttpMessageHandlerclasse de test aura un HttpRequestHandlerdélégué que les appelants peuvent définir et gérer simplement HttpRequestcomme ils le souhaitent.

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

Vous pouvez utiliser une instance de cette classe pour créer une instance HttpClient concrète. Via le délégué HttpRequestHandler, vous avez un contrôle total sur les requêtes http sortantes de HttpClient.

Dogu Arslan
la source
1

Inspiré de la réponse de PointZeroTwo , voici un exemple utilisant NUnit et FakeItEasy .

SystemUnderTest dans cet exemple se trouve la classe que vous voulez tester - aucun exemple de contenu n'est donné mais je suppose que vous l'avez déjà!

[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
la source
et si je veux passer un paramètre à la méthode, mentionné par rapport à "x.Method.Name" ..?
Shailesh
0

Il y aurait peut-être du code à changer dans votre projet actuel, mais pour les nouveaux projets, vous devriez absolument envisager d'utiliser Flurl.

https://flurl.dev

Il s'agit d'une bibliothèque cliente HTTP pour .NET avec une interface fluide qui permet spécifiquement la testabilité du code qui l'utilise pour effectuer des requêtes HTTP.

Il existe de nombreux exemples de code sur le site Web, mais en un mot, vous l'utilisez comme ceci dans votre code.

Ajoutez les utilisations.

using Flurl;
using Flurl.Http;

Envoyez une requête get et lisez la réponse.

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

Dans les tests unitaires, Flurl agit comme un simulacre qui peut être configuré pour se comporter comme vous le souhaitez et également pour vérifier les appels qui ont été effectués.

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
la source
0

Après une recherche minutieuse, j'ai trouvé la meilleure approche pour y parvenir.

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

Comme vous l'avez remarqué, j'ai utilisé des packages Moq et Moq.Protected.

Amin Mohamed
la source
0

Pour ajouter mes 2 cents. Pour simuler des méthodes de requête http spécifiques, soit Get ou Post. Cela a fonctionné pour moi.

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
la source