Mocking HttpClient dans les tests unitaires

J’ai quelques problèmes à essayer d’envelopper 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 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) à partir de mon back-end. Cependant, je veux écrire des tests unitaires pour cette classe, et évidemment, je ne veux pas écrire de tests sur le back-end réel, mais plutôt sur des tests simulés. J’ai essayé de faire une bonne réponse à Google 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.

Votre interface expose la classe concrète HttpClient . Par conséquent, toutes les classes qui utilisent cette interface sont liées à cette classe, ce qui signifie qu’elle ne peut pas être moquée.

HttpClient n’hérite d’aucune interface, vous devrez donc écrire vous-même. Je suggère un motif de décorateur :

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

Et votre classe ressemblera à ceci:

 public class HttpClientHandler : IHttpHandler { private HttpClient _client = new HttpClient(); public HttpResponseMessage Get(ssortingng url) { return GetAsync(url).Result; } public HttpResponseMessage Post(ssortingng url, HttpContent content) { return PostAsync(url, content).Result; } public async Task GetAsync(ssortingng url) { return await _client.GetAsync(url); } public async Task PostAsync(ssortingng url, HttpContent content) { return await _client.PostAsync(url, content); } } 

Le point HttpClientHandler de tout cela est que HttpClientHandler crée son propre HttpClient , vous pouvez bien sûr créer plusieurs classes qui implémentent IHttpHandler de différentes manières.

Le principal problème avec cette approche est que vous écrivez efficacement une classe qui appelle uniquement 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 approche bien meilleure que la mienne). La vie serait beaucoup plus facile si HttpClient avait une interface à laquelle on pouvait se moquer, malheureusement ce n’est pas le cas.

Cet exemple n’est pas le ticket d’or cependant. IHttpHandler s’appuie toujours sur HttpResponseMessage , qui appartient à l’ System.Net.Http noms System.Net.Http . Par conséquent, si vous avez besoin d’autres implémentations que HttpClient , vous devrez effectuer un mappage pour convertir leurs réponses en objects HttpResponseMessage . Ce n’est bien sûr qu’un problème si vous devez utiliser plusieurs implémentations d’ IHttpHandler mais cela ne semble pas être le cas, ce n’est pas la fin du monde, mais c’est une chose à laquelle vous devez penser.

Quoi qu’il en soit, vous pouvez simplement IHttpHandler sans avoir à vous soucier de la classe HttpClient concrète car elle a été retirée.

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

L’extensibilité de HttpClient réside dans le HttpMessageHandler transmis au constructeur. Son but est de permettre des implémentations spécifiques à la plate-forme, mais vous pouvez également vous y moquer. Il n’est pas nécessaire de créer un wrapper de 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.ReadAsSsortingngAsync(); // No network connection required Console.Write(json); // {'name' : 'Test McGee'} 

Je suis d’accord avec certaines des autres réponses selon lesquelles la meilleure approche consiste à simuler HttpMessageHandler plutôt qu’à 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é avec une dependency injection.

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

Se moquer de HttpMessageHandler peut être un peu compliqué 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(ssortingng[] 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 GetContentSize(ssortingng uri) { var response = await _httpClient.GetAsync( uri ); var content = await response.Content.ReadAsSsortingngAsync(); return content.Length; } private readonly HttpClient _httpClient; } public class SiteAnalyzerTests { [Fact] public async void GetContentSizeReturnsCorrectLength() { // Arrange const ssortingng testContent = "test content"; var mockMessageHandler = new Mock(); mockMessageHandler.Protected() .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new SsortingngContent(testContent) }); var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object)); // Act var result = await underTest.GetContentSize("http://anyurl"); // Assert Assert.Equal(testContent.Length, result); } } } 

C’est une question courante, et j’ai beaucoup insisté sur la possibilité de me moquer de HttpClient, mais je pense que j’ai finalement réalisé que vous ne devriez pas vous moquer de HttpClient. Cela semble logique, mais je pense que nous avons subi un lavage de cerveau avec des choses que nous voyons dans les bibliothèques open source.

Nous voyons souvent des “Clients” dans notre code afin que nous puissions les tester isolément. Nous essayons donc automatiquement d’appliquer le même principe à HttpClient. HttpClient fait vraiment 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 la conception de vos services est même le HttpMessageHandler, car c’est ce qui renvoie la réponse, et vous pouvez vous en moquer.

Il convient également de noter que vous devriez probablement commencer à traiter HttpClient comme une affaire plus importante. Par exemple: Gardez votre installation de nouveaux clients HTTP à un minimum. Les réutiliser, ils sont conçus pour être réutilisés et utilisent moins de ressources si vous le faites. Si vous commencez à le traiter comme une affaire plus importante, il sera beaucoup plus faux de vouloir se moquer de lui et 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. Encore mieux, des “services” abstraits qui utilisent HttpClient qui vous permettent d’injecter un gestionnaire, et de l’utiliser à la place comme dépendance injectable. Ensuite, dans vos tests, vous pouvez simuler le gestionnaire pour contrôler la réponse à la configuration de vos tests.

Envelopper HttpClient est une perte de temps insensée.

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

Comme mentionné dans les commentaires, vous devez séparer le HttpClient pour ne pas y être couplé. J’ai fait quelque chose de similaire par le passé. Je vais essayer d’adapter ce que j’ai fait avec ce que vous essayez de faire.

Commencez par examiner la classe HttpClient et HttpClient fonctionnalité requirejse.

Voici une possibilité:

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

Encore une fois, comme indiqué précédemment, c’était à des fins particulières. J’ai complètement supprimé la plupart des dépendances pour tout ce qui concernait HttpClient et je me suis concentré sur ce que je voulais retourner. Vous devez évaluer la manière dont vous voulez HttpClient le HttpClient pour ne fournir que les fonctionnalités nécessaires.

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

Je recommanderais même de IHttpHandler complètement IHttpHandler et d’utiliser l’abstraction IHttpClient . Mais je ne fais que choisir car vous pouvez remplacer le corps de votre interface de gestionnaire par les membres du client abstrait.

Une implémentation de IHttpClient peut alors être utilisée pour encapsuler / adapter un HttpClient réel / concret ou tout autre object, qui peut être utilisé pour créer des requêtes HTTP, car ce que vous vouliez vraiment, c’était un service fournissant cette fonctionnalité à HttpClient Plus précisément. L’utilisation de l’abstraction est une approche propre (mon avis) et SOLID et peut rendre votre code plus facile à gérer si vous avez besoin de changer le client sous-jacent pour autre chose lorsque l’infrastructure change.

Voici un extrait de la manière dont une implémentation pourrait être réalisée.

 ///  /// HTTP Client adaptor wraps a  /// that contains a reference to  ///  public sealed class HttpClientAdaptor : IHttpClient { HttpClient httpClient; public HttpClientAdaptor(IHttpClientFactory httpClientFactory) { httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**); } //...other code ///  /// Send a GET request to the specified Uri as an asynchronous operation. ///  /// Response type /// The Uri the request is sent to ///  public async System.Threading.Tasks.Task GetAsync(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 ssortingng responseBodyAsText = await response.Content.ReadAsSsortingngAsync().ConfigureAwait(false); //desrialize it result = deserializeJsonToObject(responseBodyAsText); } } catch (Exception ex) { Log.Error(ex); } return result; } //...other code } 

Comme vous pouvez le voir dans l’exemple ci-dessus, une grande partie des HttpClient généralement associées à l’utilisation de HttpClient est cachée derrière l’abstraction.

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

 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(); httpClientMock.Setup(c => c.GetAsync(It.IsAny())) .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(); } 

Sur la base des 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.GetSsortingngAsync("http://some.fake.url"); Assert.AreEqual("Content as ssortingng", content); } } public class MockHttpMessageHandler : HttpMessageHandler { protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new SsortingngContent("Content as ssortingng") }; return await Task.FromResult(responseMessage); } } 

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 en vie pour des performances optimales. Ce type de structure vous permet de le faire. Le HttpMessageHandler est également une classe abstraite et donc modifiable. Votre méthode de test ressemblerait alors à ceci:

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

Cela vous permet de tester votre logique tout en raillant le comportement de HttpClient.

Désolé les gars, après avoir écrit ceci et essayé moi-même, je me suis rendu compte que vous ne pouvez pas simuler les méthodes protégées sur le HttpMessageHandler. J’ai ensuite ajouté le code suivant pour permettre l’injection d’un modèle approprié.

 public interface IMockHttpMessageHandler { Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); } public class MockHttpMessageHandler : HttpMessageHandler { private readonly IMockHttpMessageHandler _realMockHandler; public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler) { _realMockHandler = realMockHandler; } protected override async Task 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(); // Set up Mock behavior here. var auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object)); // Act // Assert } 

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

La méthode la plus simple pour HttpClient était donc de simplement HttpClient cette méthode:

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

et votre code peut appeler la plupart des méthodes de classe HttpClient (mais pas toutes), y compris

 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

Une alternative consisterait à configurer un serveur HTTP de remplacement qui renvoie des réponses prédéfinies en fonction du modèle correspondant à l’URL de la requête, ce qui signifie que vous testez les requêtes HTTP réelles et non les simulacres. Historiquement, cela aurait nécessité un effort de développement important et il aurait été beaucoup plus lent à être pris en compte pour 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. Le programme d’installation est constitué 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.

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

GitHub MockHttp

PM> Install-Package RichardSzalay.MockHttp

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 complète. L’appelant (par exemple, la couche de service de votre application) n’est pas au courant 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.ReadAsSsortingngAsync(); // No network connection required Console.Write(json); // {'name' : 'Test McGee'} 

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 “Ajouter un assemblage de faux”
  2. Placez votre code dans la méthode de test unitaire sous ShimsContext.Create () using. De cette façon, vous pouvez isoler le code dans lequel vous prévoyez de simuler le HttpClient.
  3. Dépend de votre implémentation et de vos tests, je suggère de mettre en œuvre toutes les actions souhaitées où vous appelez une méthode sur le 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.GetAsyncSsortingng = (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(); } } 

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 GetAsync(ssortingng uri, Dictionary 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 GetAsync(ssortingng uri, Dictionary headers = null) { ... rawResponse = await GetAsync(uri, headers); ... } } 

et le simulacre est:

  [TestInitialize] public void Initialize() { ... _httpHelper = new Mock(_logHelper.Object) { CallBase = true }; ... } [TestMethod] public async Task SuccessStatusCode_WithAuthHeader() { ... _httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns( Task.Factory.StartNew(() => { return new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new SsortingngContent(JsonConvert.SerializeObject(_testData)) }; }) ); var result = await _httpHelper.Object.GetAsync(...); Assert.AreEqual(...); }