Zombar de HttpClient em testes de unidade

111

Eu tenho alguns problemas ao tentar quebrar meu código para ser usado em testes de unidade. Os problemas são estes. Tenho a interface IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

E a classe que o usa, HttpHandler:

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

E então a classe Connection, que usa simpleIOC para injetar a implementação do cliente:

public class Connection
{
    private IHttpHandler _httpClient;

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

E então eu tenho um projeto de teste de unidade que tem esta 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?     

}

Agora, obviamente, terei métodos na classe Connection que recuperarão dados (JSON) de um back end. No entanto, quero escrever testes de unidade para esta classe e, obviamente, não quero escrever testes contra o backend real, em vez de um simulado. Tentei pesquisar no Google uma boa resposta para isso, sem muito sucesso. Eu posso e já usei o Moq para simular antes, mas nunca em algo como httpClient. Como devo abordar este problema?

Desde já, obrigado.

tjugg
fonte
1
Expor um HttpClientem sua interface é onde está o problema. Você está forçando seu cliente a usar a HttpClientclasse concreta. Em vez disso, você deve expor uma abstração do HttpClient.
Mike Eason,
Você pode explicar um pouco mais a fundo? Como devo construir o construtor de classes de conexão porque não quero nenhuma dependência de HttpClient em outras classes que usem a classe Connection. Por exemplo, eu não quero passar HttpClient concerete no construtor de Connection porque isso tornaria todas as outras classes que usam Connection dependentes de HttpClient?
tjugg
Sem interesse, o que você pesquisou no Google? Aparentemente, o mockhttp pode usar algumas melhorias de SEO.
Richard Szalay,
@Mike - conforme mencionado na minha resposta, não há realmente necessidade de abstrair HttpClient. É perfeitamente testável como está. Tenho vários projetos que têm suítes de teste sem backend usando esse método.
Richard Szalay,

Respostas:

37

Sua interface expõe a HttpClientclasse concreta , portanto, quaisquer classes que usam essa interface estão vinculadas a ela, o que significa que não pode ser simulada.

HttpClientnão herda de nenhuma interface, portanto, você terá que escrever a sua própria. Eu sugiro um padrão de decorador :

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

E sua aula será assim:

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

O ponto em tudo isso é que HttpClientHandlercria o seu próprio HttpClient, então, é claro, você pode criar várias classes que implementam de IHttpHandlermaneiras diferentes.

O principal problema com esta abordagem é que você está efetivamente escrevendo uma classe que só chama métodos em outra classe, no entanto, você poderia criar uma classe que herda de HttpClient(Veja o exemplo de Nkosi , é uma abordagem muito melhor do que o meu). A vida seria muito mais fácil se HttpClienttivesse uma interface da qual você pudesse zombar, infelizmente não.

No entanto, este exemplo não é o bilhete dourado. IHttpHandlerainda depende de HttpResponseMessage, que pertence ao System.Net.Httpnamespace, portanto, se você precisar de outras implementações diferentes HttpClient, terá que realizar algum tipo de mapeamento para converter suas respostas em HttpResponseMessageobjetos. Isso, é claro, só é um problema se você precisar usar várias implementações de, IHttpHandlermas não parece que você faça, então não é o fim do mundo, mas é algo em que se pensar.

De qualquer forma, você pode simplesmente zombar IHttpHandlersem ter que se preocupar com a HttpClientclasse concreta conforme ela foi abstraída.

Eu recomendo testar os métodos não assíncronos , pois eles ainda chamam os métodos assíncronos, mas sem o incômodo de ter que se preocupar com os métodos assíncronos de teste de unidade, veja aqui

Mike Eason
fonte
Isso realmente responde à minha pergunta. A resposta de Nkosis também está correta, então não tenho certeza de qual devo aceitar como resposta, mas irei com esta. Obrigado a ambos pelo esforço
tjugg
@tjugg Fico feliz em ajudar. Sinta-se à vontade para votar nas respostas se você as considerar úteis.
Nkosi
3
É importante notar que a principal diferença entre esta resposta e a de Nkosi é que esta é uma abstração muito mais fina. Fino provavelmente é bom para um objeto humilde
Ben Aaronson,
228

A extensibilidade do HttpClient reside no HttpMessageHandlerpassado para o construtor. Sua intenção é permitir implementações específicas da plataforma, mas você também pode simular isso. Não há necessidade de criar um wrapper decorador para HttpClient.

Se você preferir um DSL em vez de Moq, tenho uma biblioteca no GitHub / Nuget que torna as coisas um pouco mais fáceis: 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
fonte
1
Então, eu apenas passaria MockHttpMessageHandler como a classe Messagehandler Httphandler? Ou como você o implementou em seus próprios projetos
tjugg
2
Ótima resposta e algo que eu não teria conhecido inicialmente. Torna o trabalho com HttpClient não tão ruim.
Bealer
6
Para pessoas que não querem lidar com a injeção no cliente, mas ainda querem testabilidade fácil, isso é trivial. Basta substituir var client = new HttpClient()com var client = ClientFactory()e configurar um campo internal static Func<HttpClient> ClientFactory = () => new HttpClient();e, ao nível de teste você pode reescrever neste campo.
Chris Marisic
3
@ChrisMarisic você está sugerindo uma forma de local de serviço para substituir a injeção. O local de serviço é um antipadrão bem conhecido, então a injeção de imho é preferível.
MarioDS de
2
@MarioDS e independentemente, você não deve injetar uma instância HttpClient de forma alguma. Se você está decidido a usar injeção de construtor para isso, você deve injetar um HttpClientFactoryas in Func<HttpClient>. Visto que vejo o HttpClient puramente como um detalhe de implementação e não uma dependência, usarei a estática conforme ilustrado acima. Estou totalmente bem com testes de manipulação de componentes internos. Se eu me preocupo com o pure-ism, colocarei servidores completos e testarei caminhos de código ao vivo. Usar qualquer tipo de simulação significa que você aceita a aproximação do comportamento, não o comportamento real.
Chris Marisic
39

Concordo com algumas das outras respostas de que a melhor abordagem é simular HttpMessageHandler em vez de envolver HttpClient. Essa resposta é única porque ainda injeta HttpClient, permitindo que seja um singleton ou gerenciado com injeção de dependência.

"HttpClient deve ser instanciado uma vez e reutilizado ao longo da vida de um aplicativo." ( Fonte ).

Zombar de HttpMessageHandler pode ser um pouco complicado porque SendAsync é protegido. Aqui está um exemplo completo, usando xunit e Moq.

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

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

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

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

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

        private readonly HttpClient _httpClient;
    }

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

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

            // Assert
            Assert.Equal(testContent.Length, result);
        }
    }
}
PointZeroTwo
fonte
1
Eu realmente gostei disso. A mockMessageHandler.Protected()era o assassino. Obrigado por este exemplo. Ele permite escrever o teste sem modificar a fonte.
tirião de
1
Para sua informação, o Moq 4.8 oferece suporte à simulação de membros protegidos fortemente tipados - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay
2
Isso parece ótimo. Além disso, o Moq suporta ReturnsAsync para que o código se pareça com.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord
Obrigado @kord, adicionei isso à resposta
PointZeroTwo
3
Existe alguma maneira de verificar se "SandAsync" foi chamado com alguns parâmetros? Tentei usar ... Protected (). Verify (...), mas parece que não funciona com métodos assíncronos.
Rroman
29

Essa é uma pergunta comum, e eu estava fortemente do lado, querendo a capacidade de zombar do HttpClient, mas acho que finalmente percebi que você não deveria zombar do HttpClient. Parece lógico fazer isso, mas acho que passamos por uma lavagem cerebral por coisas que vemos em bibliotecas de código aberto.

Freqüentemente, vemos "Clientes" por aí que simulamos em nosso código para que possamos testar isoladamente, então tentamos aplicar automaticamente o mesmo princípio ao HttpClient. O HttpClient realmente faz muito; você pode pensar nele como um gerenciador para HttpMessageHandler, então você não quer zombar disso, e é por isso que ele ainda não tem uma interface. A parte na qual você está realmente interessado para teste de unidade, ou até mesmo para projetar seus serviços, é o HttpMessageHandler, já que é ele que retorna a resposta, e você pode zombar disso.

Também vale a pena ressaltar que você provavelmente deve começar a tratar o HttpClient como um negócio maior. Por exemplo: reduza ao mínimo a introdução de novos HttpClients. Reutilize-os, eles são projetados para serem reutilizados e usar uma tonelada de recursos a menos se você o fizer. Se você começar a tratá-lo como algo importante, parecerá muito mais errado querer zombar dele e agora o manipulador de mensagens começará a ser o que você está injetando, não o cliente.

Em outras palavras, projete suas dependências em torno do manipulador em vez do cliente. Melhor ainda, "serviços" abstratos que usam HttpClient, que permitem injetar um manipulador e usá-lo como sua dependência injetável. Então, em seus testes, você pode simular o manipulador para controlar a resposta para configurar seus testes.

Empacotar HttpClient é uma perda de tempo insana.

Atualização: Veja o exemplo de Joshua Dooms. É exatamente o que estou recomendando.

Sinestésico
fonte
17

Como também mencionado nos comentários, você precisa abstrair o HttpClientpara não ser acoplado a ele. Já fiz algo semelhante no passado. Vou tentar adaptar o que fiz com o que você está tentando fazer.

Primeiro, olhe para a HttpClientclasse e decida quais funcionalidades são fornecidas e são necessárias.

Aqui está uma possibilidade:

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

Novamente, como afirmado antes, isso era para fins específicos. Eu abstraia completamente a maioria das dependências para qualquer coisa que lide HttpCliente foque no que eu queria que fosse devolvido. Você deve avaliar como deseja abstrair o HttpClientpara fornecer apenas a funcionalidade necessária desejada.

Isso agora permitirá que você simule apenas o que precisa ser testado.

Eu até recomendaria acabar com tudo IHttpHandlere usar a HttpClientabstração IHttpClient. Mas não estou escolhendo, já que você pode substituir o corpo da interface do manipulador pelos membros do cliente abstraído.

Uma implementação de IHttpClientpode então ser usada para empacotar / adaptar um HttpClientobjeto real / concreto ou qualquer outro objeto para esse assunto, que pode ser usado para fazer solicitações HTTP, pois o que você realmente queria era um serviço que fornecesse essa funcionalidade conforme HttpClientaplicável especificamente. Usar a abstração é uma abordagem limpa (minha opinião) e SOLID e pode tornar seu código mais fácil de manter se você precisar trocar o cliente subjacente por outra coisa conforme a estrutura muda.

Aqui está um trecho de como uma implementação poderia ser feita.

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

Como você pode ver no exemplo acima, muito do trabalho pesado geralmente associado ao uso HttpClientestá oculto por trás da abstração.

Sua classe de conexão pode então ser injetada com o cliente abstraído

public class Connection
{
    private IHttpClient _httpClient;

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

Seu teste pode simular o que é necessário para o seu 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
fonte
ESTA é a resposta. Uma abstração acima HttpCliente um adaptador para criar sua instância específica usando HttpClientFactory. Fazer isso torna trivial o teste da lógica além da solicitação HTTP, que é o objetivo aqui.
pimbrouwers
13

Com base nas outras respostas, sugiro este código, que não possui dependências externas:

[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);
    }
}
pio
fonte
4
Você está testando efetivamente o seu mock. O verdadeiro poder de um mock é que você pode definir expectativas e alterar seu comportamento em cada teste. O fato de você mesmo ter que implementar alguns HttpMessageHandlertorna isso quase impossível - e você precisa porque os métodos são protected internal.
MarioDS de
3
@MarioDS Acho que a questão é que você pode simular a resposta HTTP para que possa testar o resto do código. Se você injetar uma fábrica que obtém o HttpClient, em testes você pode fornecer esse HttpClient.
chris31389
13

Acho que o problema é que você só está um pouco de cabeça para baixo.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Se você olhar para a classe acima, acho que é isso que você quer. A Microsoft recomenda manter o cliente ativo para um desempenho ideal, portanto, esse tipo de estrutura permite que você faça isso. Além disso, o HttpMessageHandler é uma classe abstrata e, portanto, pode ser zombada. Seu método de teste ficaria assim:

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

Isso permite que você teste sua lógica enquanto simula o comportamento do HttpClient.

Desculpe pessoal, depois de escrever isso e tentar sozinho, percebi que você não pode zombar dos métodos protegidos no HttpMessageHandler. Posteriormente, adicionei o seguinte código para permitir a injeção de um mock adequado.

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

Os testes escritos com isso se parecem com o seguinte:

[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
fonte
9

Um dos meus colegas notou que a maioria dos HttpClientmétodos chama por SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)baixo do capô, que é um método virtual fora de HttpMessageInvoker:

Portanto, de longe, a maneira mais fácil de zombar HttpClientera simplesmente zombar desse método específico:

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

e seu código pode chamar a maioria (mas não todos) dos HttpClientmétodos de classe, incluindo um

httpClient.SendAsync(req)

Verifique aqui para confirmar https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

Adão
fonte
1
No entanto, isso não funciona para nenhum código que chame SendAsync(HttpRequestMessage)diretamente. Se você puder modificar seu código para não usar essa função de conveniência, simular HttpClient diretamente substituindo-o SendAsyncé, na verdade, a solução mais limpa que encontrei.
Dylan Nicholson
8

Uma alternativa seria configurar um servidor HTTP stub que retornasse respostas prontas com base no padrão que corresponde ao url da solicitação, o que significa que você testa solicitações HTTP reais, não simulações. Historicamente, isso exigiria um esforço significativo de desenvolvimento e demoraria muito para ser considerado para testes de unidade. No entanto, a biblioteca OSS WireMock.net é fácil de usar e rápida o suficiente para ser executada com muitos testes, portanto, pode valer a pena considerar. A configuração consiste em algumas linhas de código:

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

Você pode encontrar mais detalhes e orientações sobre como usar wiremock em testes aqui.

Alastairtree
fonte
8

Aqui está uma solução simples, que funcionou bem para mim.

Usando a biblioteca de simulação do moq.

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

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

var subjectUnderTest = new MyTestClass(httpClient);

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

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

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

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

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

j7nn7k
fonte
Eu também usei isso com sucesso. Eu prefiro isso do que ficar ainda mais na dependência de nuget e você aprenderá um pouco mais sobre o que está acontecendo nos bastidores também. O bom é que a maioria dos métodos acaba usando de SendAsyncqualquer maneira, então nenhuma configuração extra é necessária.
Steve Pettifer
4

Não estou convencido por muitas das respostas.

Em primeiro lugar, imagine que você deseja fazer um teste de unidade de um método que usa HttpClient. Você não deve instanciar HttpClientdiretamente em sua implementação. Você deve injetar uma fábrica com a responsabilidade de fornecer uma instância de HttpClientpara você. Dessa forma, você pode fazer o mock mais tarde naquela fábrica e retornar o HttpClientque quiser (por exemplo: um mock HttpCliente não o real).

Então, você teria uma fábrica como a seguinte:

public interface IHttpClientFactory
{
    HttpClient Create();
}

E uma implementação:

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

Claro, você precisaria registrar em seu contêiner IoC esta implementação. Se você usar o Autofac, seria algo como:

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

Agora você teria uma implementação adequada e testável. Imagine que seu método seja algo como:

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

Agora, a parte de teste. HttpClientestende HttpMessageHandler, que é abstrato. Vamos criar um "mock" de HttpMessageHandlerque aceita um delegado para que, ao usarmos o mock, possamos também configurar cada comportamento para cada teste.

public class MockHttpMessageHandler 
    : HttpMessageHandler
{
    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;

    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
    {
        _sendAsyncFunc = sendAsyncFunc;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _sendAsyncFunc.Invoke(request, cancellationToken);
    }
}

E agora, e com a ajuda do Moq (e FluentAssertions, uma biblioteca que torna os testes de unidade mais legíveis), temos tudo o que é necessário para testar nosso método PostAsync que usa 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);
        }
    }
}

Obviamente, esse teste é bobo, e estamos realmente testando nosso mock. Mas você entendeu. Você deve testar uma lógica significativa, dependendo de sua implementação, como ..

  • se o status do código da resposta não for 201, ele deve lançar uma exceção?
  • se o texto da resposta não puder ser analisado, o que deve acontecer?
  • etc.

O objetivo desta resposta foi testar algo que usa HttpClient e esta é uma maneira limpa e agradável de fazer isso.

diegosasw
fonte
4

Entrando na festa um pouco tarde, mas gosto de usar wiremocking ( https://github.com/WireMock-Net/WireMock.Net ) sempre que possível no teste de integração de um microsserviço central dotnet com dependências REST downstream.

Ao implementar um TestHttpClientFactory estendendo o IHttpClientFactory, podemos substituir o método

HttpClient CreateClient (nome da string)

Portanto, ao usar os clientes nomeados em seu aplicativo, você tem o controle de devolver um HttpClient conectado à sua wiremock.

O bom dessa abordagem é que você não está mudando nada no aplicativo que está testando e permite que os testes de integração do curso façam uma solicitação REST real para o seu serviço e simulem o json (ou qualquer outro) que a solicitação downstream real deva retornar. Isso leva a testes concisos e o mínimo de simulação possível em seu aplicativo.

    public class TestHttpClientFactory : IHttpClientFactory 
{
    public HttpClient CreateClient(string name)
    {
        var httpClient = new HttpClient
        {
            BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
            // G.Config is our singleton config access, so the endpoint 
            // to the running wiremock is used in the test
        };
        return httpClient;
    }
}

e

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

Uma vez que HttpClientuse o SendAsyncmétodo para realizar tudo HTTP Requests, você pode usar o método override SendAsynce simular o HttpClient.

Para esse envoltório criando HttpClientum interface, algo como abaixo

public interface IServiceHelper
{
    HttpClient GetClient();
}

Em seguida, use acima interfacepara injeção de dependência em seu serviço, exemplo abaixo

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

Agora, no projeto de teste de unidade, crie uma classe auxiliar para simulação SendAsync. Aqui está uma FakeHttpResponseHandlerclasse que inheriting DelegatingHandlerfornecerá uma opção para sobrescrever o SendAsyncmétodo. Após substituir o SendAsyncmétodo é necessário configurar uma resposta para cada um HTTP Requestque está chamando o SendAsyncmétodo, para isso crie um Dictionarycom keyas Urie valueas HttpResponseMessagepara que sempre que houver um HTTP Requeste se as Uricorrespondências SendAsyncretornem o configurado 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")
        });
    }
}

Crie uma nova implementação para a IServiceHelperestrutura de simulação ou como a seguir. Esta FakeServiceHelperclasse que podemos usar para injetar a FakeHttpResponseHandlerclasse para que sempre que o HttpClientcriado por este classele vai usar FakeHttpResponseHandler classem vez da execução efectiva.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

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

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

E em teste configure FakeHttpResponseHandler classadicionando o Urie o esperado HttpResponseMessage. A Urideve ser o actual serviceponto final Uripara que quando o overridden SendAsyncmétodo é chamado de real serviceimplementação ele irá corresponder a Urino Dictionarye responder com o configurado HttpResponseMessage. Depois de configurar, injete o FakeHttpResponseHandler objectpara a IServiceHelperimplementação falsa . Em seguida, injete o FakeServiceHelper classno serviço real que fará com que o serviço real use o override SendAsyncmétodo.

[TestClass]
public class SampleServiceTest
{
    private FakeHttpResponseHandler fakeHttpResponseHandler;

    [TestInitialize]
    public void Initialize()
    {
        fakeHttpResponseHandler = new FakeHttpResponseHandler();
    }

    [TestMethod]
    public async Task GetMethodShouldReturnFakeResponse()
    {
        Uri uri = new Uri("http://www.dummyurl.com/api/controller/");
        const int dummyParam = 123456;
        const string expectdBody = "Expected Response";

        var expectedHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(expectdBody)
        };

        fakeHttpResponseHandler.AddFakeServiceResponse(uri, expectedHttpResponseMessage, dummyParam);

        var fakeServiceHelper = new FakeServiceHelper(fakeHttpResponseHandler);

        var sut = new SampleService(fakeServiceHelper);

        var response = await sut.Get(dummyParam);

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

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(expectdBody, responseBody);
    }
}

GitHub Link: que está tendo implementação de amostra

ghosh-arun
fonte
Embora este código possa resolver a questão, incluir uma explicação de como e por que isso resolve o problema realmente ajudaria a melhorar a qualidade da sua postagem e provavelmente resultaria em mais votos positivos. Lembre-se de que você está respondendo às perguntas dos leitores no futuro, não apenas da pessoa que está perguntando agora. Por favor edite sua resposta para adicionar explicações e dar uma indicação do que limitações e premissas se aplicam.
Богдан Опир
Obrigado @ БогданОпир pelo feedback atualizado explicação.
ghosh-arun
1

Você pode usar a biblioteca RichardSzalay MockHttp que simula o HttpMessageHandler e pode retornar um objeto HttpClient para ser usado durante os testes.

GitHub MockHttp

PM> Instalar-Pacote RichardSzalay.MockHttp

Da documentação do GitHub

MockHttp define um HttpMessageHandler substituto, o mecanismo que aciona o HttpClient, que fornece uma API de configuração fluente e uma resposta pronta. O chamador (por exemplo, a camada de serviço do seu aplicativo) permanece inconsciente de sua presença.

Exemplo do 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
fonte
1

Essa é uma pergunta antiga, mas sinto vontade de estender as respostas com uma solução que não encontrei aqui.
Você pode simular o conjunto da Microsoft (System.Net.Http) e usar o ShinsContext durante o teste.

  1. No VS 2017, clique com o botão direito no assembly System.Net.Http e escolha "Add Fakes Assembly"
  2. Coloque seu código no método de teste de unidade em um ShimsContext.Create () usando. Dessa forma, você pode isolar o código no qual está planejando falsificar o HttpClient.
  3. Depende da sua implementação e teste, sugiro implementar todo o desempenho desejado onde você chama um método no HttpClient e deseja falsificar o valor retornado. Usar ShimHttpClient.AllInstances irá falsificar sua implementação em todas as instâncias criadas durante seu teste. Por exemplo, se você deseja falsificar o método GetAsync (), faça o seguinte:

    [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
fonte
1

Fiz algo muito simples, pois estava em um ambiente 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);

        ...
    }

}

e a simulação é:

    [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
fonte
1

Tudo que você precisa é de uma versão de teste da HttpMessageHandlerclasse que você passa para o HttpClientctor. O ponto principal é que sua HttpMessageHandlerclasse de teste terá um HttpRequestHandlerdelegado que os chamadores podem definir e simplesmente manipular HttpRequestda maneira que quiserem.

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

Você pode usar uma instância desta classe para criar uma instância concreta de HttpClient. Por meio do delegado HttpRequestHandler, você tem controle total sobre as solicitações http de saída de HttpClient.

Dogu Arslan
fonte
1

Inspirado pela resposta do PointZeroTwo , aqui está um exemplo usando NUnit e FakeItEasy .

SystemUnderTest neste exemplo está a classe que você deseja testar - nenhum conteúdo de amostra fornecido para ela, mas presumo que você já o tenha!

[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
fonte
e se eu quiser passar um parâmetro para o método, mencionado em "x.Method.Name" ..?
Shailesh
0

Talvez haja algum código a ser alterado em seu projeto atual, mas para novos projetos, você deve considerar o uso de Flurl.

https://flurl.dev

É uma biblioteca cliente HTTP para .NET com uma interface fluente que habilita especificamente a capacidade de teste para o código que a usa para fazer solicitações HTTP.

Existem muitos exemplos de código no site, mas em poucas palavras, você os usa assim em seu código.

Adicione os usos.

using Flurl;
using Flurl.Http;

Envie uma solicitação get e leia a resposta.

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

Nos testes unitários o Flurl atua como um mock que pode ser configurado para se comportar como desejado e também para verificar as chamadas que foram feitas.

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
fonte
0

Depois de pesquisar cuidadosamente, descobri a melhor abordagem para fazer isso.

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

Como você notou, usei os pacotes Moq e Moq.Protected.

Amin Mohamed
fonte
0

Para adicionar meus 2 centavos. Para simular métodos de solicitação http específicos, Get ou Post. Isso funcionou para mim.

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
fonte