web-dev-qa-db-fra.com

Mock HttpClient utilisant Moq

J'aime tester unitairement une classe qui utilise HttpClient. Nous avons injecté l'objet HttpClient dans le constructeur de classe.

public class ClassA : IClassA
{
    private readonly HttpClient _httpClient;

    public ClassA(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<HttpResponseMessage> SendRequest(SomeObject someObject)
    {
        //Do some stuff

        var request = new HttpRequestMessage(HttpMethod.Post, "http://some-domain.in");

        //Build the request

        var response = await _httpClient.SendAsync(request);

        return response;
    }
}

Maintenant, nous aimons tester unitairement le ClassA.SendRequest méthode. Nous utilisons Ms Test pour le framework de tests unitaires et Moq pour la simulation.

Lorsque nous avons essayé de nous moquer de HttpClient, il lance NotSupportedException.

[TestMethod]
public async Task SendRequestAsync_Test()
{
    var mockHttpClient = new Mock<HttpClient>();

    mockHttpClient.Setup(
        m => m.SendAsync(It.IsAny<HttpRequestMessage>()))
    .Returns(() => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
}

Comment pouvons-nous résoudre ce problème?

9
Amitava Karan

Cette méthode de surcharge particulière n'est pas virtuelle et ne peut donc pas être remplacée par Moq.

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);

C'est pourquoi il lance NotSupportedException

La méthode virtuelle que vous recherchez est cette méthode

public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);

Cependant, se moquer de HttpClient n'est pas aussi simple qu'il y paraît avec son gestionnaire de messages interne.

Je suggère d'utiliser un client concret avec un talon de gestionnaire de messages personnalisé qui permettra plus de flexibilité lors de la simulation de la demande.

Voici un exemple de talon de gestionnaire délégué.

public class DelegatingHandlerStub : DelegatingHandler {
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _handlerFunc;
    public DelegatingHandlerStub() {
        _handlerFunc = (request, cancellationToken) => Task.FromResult(request.CreateResponse(HttpStatusCode.OK));
    }

    public DelegatingHandlerStub(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handlerFunc) {
        _handlerFunc = handlerFunc;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
        return _handlerFunc(request, cancellationToken);
    }
}

Notez que le constructeur par défaut fait essentiellement ce que vous tentiez de vous moquer auparavant. Il permet également des scénarios plus personnalisés avec un délégué pour la demande.

Avec le talon, le test peut être refactorisé en quelque chose comme

public async Task _SendRequestAsync_Test() {
    //Arrange           
    var handlerStub = new DelegatingHandlerStub();
    var client = new HttpClient(handlerStub);
    var sut = new ClassA(client);
    var obj = new SomeObject() {
        //Populate
    };

    //Act
    var response = await sut.SendRequest(obj);

    //Assert
    Assert.IsNotNull(response);
    Assert.IsTrue(response.IsSuccessStatusCode);
}
11
Nkosi

Se moquer correctement avec HttpClient est un travail difficile car il a été écrit avant que la plupart des gens ne fassent des tests unitaires dans dotnet. Parfois, je configure un serveur HTTP stub qui renvoie des réponses prédéfinies basées sur un modèle correspondant à l'URL de la demande, ce qui signifie que vous testez de vraies demandes HTTP non pas des mocks mais vers un serveur localhost. L'utilisation de WireMock.net rend cela très facile et fonctionne assez rapidement pour satisfaire la plupart de mes besoins de tests unitaires.

Donc au lieu de http://some-domain.in utilisez une configuration de serveur localhost sur un port, puis:

var server = FluentMockServer.Start(/*server and port can be setup here*/);
server.Given(
      Request.Create()
      .WithPath("/").UsingPost()
   )
   .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