web-dev-qa-db-fra.com

Se moquer de HttpClient dans les tests unitaires

J'ai quelques problèmes pour essayer d'envelopper mon code pour l'utiliser dans les tests unitaires. La question est la suivante. 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 ensuite la classe Connection, qui utilise simpleIOC pour injecter l'implémentation du 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) d'un back-end. Cependant, je veux écrire des tests unitaires pour cette classe et, bien évidemment, je ne veux pas écrire de tests sur le back-end réel, mais sur un simulacre. 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 devrais-je aborder ce problème?

Merci d'avance.

71
tjugg

Votre interface expose la classe HttpClient concrète. Par conséquent, toutes les classes utilisant cette interface sont liées à celle-ci. Cela signifie qu’elle ne peut pas être simulée.

HttpClient n'hérite d'aucune interface, vous devrez donc écrire la vôtre. Je suggère un motif decorator-like:

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 fait est que HttpClientHandler crée sa propre HttpClient. Vous pouvez ensuite créer plusieurs classes qui implémentent IHttpHandler de différentes manières.

Le problème principal de cette approche est que vous écrivez effectivement une classe qui appelle simplement des méthodes dans une autre classe. Cependant, vous pouvez créer une classe qui inherits de HttpClient (voir l'exemple de Nkosi, c'est une bien meilleure approche. que le mien). La vie serait beaucoup plus facile si HttpClient avait une interface que vous pouviez vous moquer, mais malheureusement pas.

Cet exemple est cependant not le ticket d’or. IHttpHandler s'appuie toujours sur HttpResponseMessage, qui appartient à l'espace de noms System.Net.Http. Par conséquent, si vous avez besoin d'implémentations autres que HttpClient, vous devrez effectuer une sorte de mappage pour convertir leurs réponses en objets HttpResponseMessage. Bien sûr, ceci n’est qu’un problème si vous devez utiliser plusieurs implémentations de IHttpHandler mais cela ne ressemble pas à ce que vous faites, ce n’est pas la fin du monde, mais une question à prendre en compte.

Quoi qu'il en soit, vous pouvez simplement simuler IHttpHandler sans avoir à vous soucier de la classe concrète HttpClient telle qu'elle a été extraite.

Je recommande de tester les méthodes non-async, car celles-ci appellent toujours les méthodes asynchrones, mais sans le souci de devoir s'inquiéter du test unitaire des méthodes asynchrones, voir ici

24
Mike Eason

L'extensibilité de HttpClient réside dans la HttpMessageHandler transmise 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 de décorateur pour HttpClient.

Si vous préférez utiliser DSL plutôt que 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'}
165
Richard Szalay

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 sens qu'elle injecte toujours HttpClient, ce qui lui permet d'être un singleton ou d'être gérée 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." ( La source ).

Se moquer de 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);
        }
    }
}
28
PointZeroTwo

Comme mentionné également dans les commentaires, vous devez résumer supprimer la HttpClient afin de ne pas y être associé. J'ai fait quelque chose de similaire dans le passé. Je vais essayer d'adapter ce que j'ai fait avec ce que vous essayez de faire.

Examinez d’abord la classe HttpClient et déterminez les fonctionnalités 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 vis-à-vis de HttpClient et je me suis concentré sur ce que je voulais obtenir. Vous devez évaluer la manière dont vous souhaitez résumer la HttpClient pour ne fournir que les fonctionnalités nécessaires.

Cela vous permettra maintenant de ne vous moquer que de ce qui doit être testé.

Je recommanderais même de supprimer complètement IHttpHandler et d’utiliser l’abstraction HttpClientIHttpClient. Mais je ne choisis rien car vous pouvez remplacer le corps de l'interface de votre gestionnaire par les membres du client extrait.

Une implémentation de IHttpClient peut ensuite être utilisée pour wrapper/adapter une variable/réelle HttpClient réelle/concrète ou tout autre objet pouvant être utilisé pour créer des demandes HTTP car ce que vous vouliez réellement était un service qui fournissait cette fonctionnalité telle que HttpClient Plus précisément. L'utilisation de l'abstraction constitue une approche propre (Mon avis) et SOLID. Elle peut rendre votre code plus facile à gérer si vous devez remplacer le client sous-jacent par un autre élément lorsque le cadre change.

Voici un extrait de la façon dont une implémentation pourrait être faite.

/// <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 constater dans l'exemple ci-dessus, une grande partie des tâches lourdes généralement associées à l'utilisation de HttpClient est masquée derrière l'abstraction. 

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

public class Connection
{
    private IHttpClient _httpClient;

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

Votre test peut alors se moquer de 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();  
}
12
Nkosi

En me basant sur les autres réponses, je suggère ce code, qui ne comporte aucune dépendance extérieure:

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

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

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

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

        return await Task.FromResult(responseMessage);
    }
}
9
pius

Je pense que le problème est que vous l'avez 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 actif pour optimiser les performances. Ce type de structure vous permet donc de le faire. De plus, HttpMessageHandler est une classe abstraite et est donc moquable. 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 en vous moquant du comportement de HttpClient.

Désolé les gars, après avoir écrit cela 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 à ce qui suit: 

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

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

Donc, de loin le moyen le plus facile de se moquer de HttpClient était simplement de 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 toutes) des méthodes de la classe HttpClient, y compris une

httpClient.SendAsync(req)

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

7
Adam

Une solution consisterait à configurer un serveur HTTP de module de remplacement 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 les demandes HTTP réelles et non les simulacres. Historiquement, cela aurait pris un effort de développement considérable et aurait été lent à prendre en compte les tests unitaires, mais la bibliothèque OSS WireMock.net est facile à utiliser et assez rapide pour être exécutée avec de nombreux tests, elle peut donc valoir la peine considérant. Le programme d'installation est composé de 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.

4
alastairtree

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

GitHub MockHttp

PM> Install-Package RichardSzalay.MockHttp

À partir de la documentation GitHub

MockHttp définit un HttpMessageHandler de remplacement, le moteur qui gère HttpClient, qui fournit une API de configuration fluide et fournit une réponse prédéfinie. L'appelant (par exemple, la couche de service de votre application) reste inconscient 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'}
2
Justin

Rejoindre la fête un peu tard, mais j'aime bien utiliser le wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) chaque fois que cela est possible dans le test d’intégration d’un microservice à noyau de points avec aval REST dépendances.

En implémentant un TestHttpClientFactory qui étend l'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 êtes en mesure de renvoyer un HttpClient câblé à votre réseau filaire.

La bonne chose à propos de cette approche est que vous ne modifiez rien dans l’application que vous testez et que vous pouvez effectuer des tests d’intégration de cours en faisant une demande REST réelle à votre service et en moquant de la requête (ou autre) la demande en aval réelle. revenir. Cela conduit à des tests concis et aussi peu moqueurs 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);
2
Markus Foss

J'ai fait quelque chose de très simple, car j'étais dans un environnement de 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 la maquette 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(...);
    }
1
Jorge Aguilar

Tout ce dont vous avez besoin est une version test de la classe HttpMessageHandler que vous passez à HttpClient ctor. Le point principal est que votre classe test HttpMessageHandler aura un délégué HttpRequestHandler que les appelants peuvent définir et gèrent simplement la HttpRequest comme 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 concrète HttpClient. Via le délégué HttpRequestHandler, vous avez le contrôle total sur les demandes http sortantes de HttpClient.

1
Dogu Arslan

Je ne suis pas convaincu par beaucoup de réponses.

Tout d'abord, imaginez que vous souhaitiez tester à l'unité une méthode utilisant HttpClient. Vous ne devez pas instancier HttpClient directement dans votre implémentation. Vous devez injecter à une usine la responsabilité de vous fournir une instance de HttpClient. De cette façon, vous pourrez vous moquer plus tard de cette usine et retourner la variable HttpClient de votre choix (par exemple: une maquette HttpClient et non la vraie).

Donc, vous auriez une usine comme celle-ci:

public interface IHttpClientFactory
{
    HttpClient Create();
}

Et une implémentation:

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

Bien entendu, 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. HttpClient étend HttpMessageHandler, ce qui est abstrait. Créons un "mock" de HttpMessageHandler qui accepte un délégué afin que, lorsque nous l'utilisons, nous puissions é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 de FluentAssertions, une bibliothèque qui rend les tests unitaires plus lisibles), nous avons tout le nécessaire pour tester 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);
        }
    }
}

Évidemment, ce test est idiot, et nous testons vraiment notre maquette. Mais vous avez l'idée. Vous devez tester une logique significative en fonction de votre implémentation, telle que ..

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

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

1
iberodev

C'est une vieille question, mais je sens l'envie d'étendre les réponses avec une solution que je n'ai pas vue ici.
Vous pouvez simuler l’assemblage Microsoft (System.Net.Http), puis utiliser ShinsContext lors du test. 

  1. Dans VS 2017, cliquez avec le bouton droit sur l'assembly System.Net.Http et choisissez "Ajouter un faux assemblage".
  2. Placez votre code dans la méthode de test unitaire sous ShimsContext.Create () avec. De cette façon, vous pouvez isoler le code où vous prévoyez de simuler le HttpClient.
  3. Dépend de votre implémentation et de vos tests, je vous suggèrerais d'implémenter tout le comportement souhaité lorsque vous appelez une méthode sur HttpClient et souhaitez simuler la valeur renvoyé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(); 
        }
    }
    
1
Zeller

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

SystemUnderTest dans cet exemple correspond à la classe que vous souhaitez tester - aucun exemple de contenu n’a été fourni, 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(...);
    }
}
0
thinkOfaNumber

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

Utilisation de la bibliothèque moq moqueuse.

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

0
j7nn7k