Mocking HttpClient en pruebas unitarias

110

Tengo algunos problemas al intentar ajustar mi código para usarlo en pruebas unitarias. El problema es este. Tengo la interfaz IHttpHandler:

public interface IHttpHandler
{
    HttpClient client { get; }
}

Y la clase que lo usa, HttpHandler:

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

Y luego la clase Connection, que usa simpleIOC para inyectar la implementación del cliente:

public class Connection
{
    private IHttpHandler _httpClient;

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

Y luego tengo un proyecto de prueba unitaria que tiene esta clase:

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?     

}

Ahora, obviamente, tendré métodos en la clase Connection que recuperarán datos (JSON) de mi back-end. Sin embargo, quiero escribir pruebas unitarias para esta clase y, obviamente, no quiero escribir pruebas contra el back-end real, sino una burla. He intentado buscar en Google una buena respuesta a esto sin gran éxito. Puedo y he usado Moq para simular antes, pero nunca en algo como httpClient. ¿Cómo debo abordar este problema?

Gracias por adelantado.

tjugg
fuente
1
Exponer un HttpClienten su interfaz es donde está el problema. Estás obligando a tu cliente a utilizar la HttpClientclase concreta. En su lugar, debe exponer una abstracción de HttpClient.
Mike Eason
¿Puedes explicarlo un poco más en profundidad? ¿Cómo debo construir el constructor de clases de conexión porque no quiero ninguna dependencia de HttpClient en otras clases para usar la clase Connection? Por ejemplo, no quiero pasar concerete HttpClient en el constructor de Connection porque eso haría que todas las demás clases que usan Connection dependan de HttpClient.
tjugg
Por interés, ¿qué buscaste en Google? Aparentemente, mockhttp podría usar algunas mejoras de SEO.
Richard Szalay
@Mike: como se mencionó en mi respuesta, realmente no hay necesidad de abstraer HttpClient. Es perfectamente comprobable como está. Tengo numerosos proyectos que tienen suites de prueba sin backend que utilizan este método.
Richard Szalay

Respuestas:

37

Su interfaz expone la HttpClientclase concreta , por lo tanto, cualquier clase que use esta interfaz está vinculada a ella, esto significa que no se puede burlar de ella.

HttpClientno hereda de ninguna interfaz por lo que tendrás que escribir la tuya propia. Sugiero un patrón similar al de un 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);
}

Y tu clase se verá así:

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

El punto en todo esto es que HttpClientHandlercrea el suyo propio HttpClient, entonces, por supuesto, podría crear múltiples clases que se implementan IHttpHandlerde diferentes maneras.

El problema principal con este enfoque es que está escribiendo efectivamente una clase que solo llama a métodos en otra clase, sin embargo, podría crear una clase que herede de HttpClient(Ver ejemplo de Nkosi , es un mejor enfoque mucho más que la mía). La vida sería mucho más fácil si HttpClienttuviera una interfaz de la que pudiera burlarse, desafortunadamente no es así.

Sin embargo, este ejemplo no es el billete dorado. IHttpHandlertodavía confía en HttpResponseMessage, que pertenece al System.Net.Httpespacio de nombres, por lo tanto, si necesita otras implementaciones además deHttpClient , tendrá que realizar algún tipo de mapeo para convertir sus respuestas en HttpResponseMessageobjetos. Por supuesto, esto es sólo un problema si es necesario utilizar múltiples implementaciones de IHttpHandler, pero no se ve como lo hace así que no es el fin del mundo, pero es algo en que pensar.

De todos modos, simplemente puede burlarse IHttpHandlersin tener que preocuparse por la HttpClientclase concreta , ya que se ha abstraído.

Recomiendo probar los métodos no asíncronos , ya que todavía llaman a los métodos asíncronos pero sin la molestia de tener que preocuparse por los métodos asíncronos de prueba unitaria, consulte aquí

Mike Eason
fuente
De hecho, esto responde a mi pregunta. La respuesta de Nkosis también es correcta, por lo que no estoy seguro de cuál debería aceptar como respuesta, pero seguiré esta. Gracias a ambos por el esfuerzo
tjugg
@tjugg Encantado de ayudar. Siéntase libre de votar las respuestas si las encuentra útiles.
Nkosi
3
Vale la pena señalar que la principal diferencia entre esta respuesta y la de Nkosi es que esta es una abstracción mucho más delgada. Delgado probablemente sea bueno para un objeto humilde
Ben Aaronson
227

La extensibilidad de HttpClient radica en el HttpMessageHandler paso al constructor. Su intención es permitir implementaciones específicas de la plataforma, pero también puede simularlo. No es necesario crear un contenedor de decorador para HttpClient.

Si prefiere un DSL a usar Moq, tengo una biblioteca en GitHub / Nuget que hace las cosas un poco más fáciles: 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
fuente
1
Entonces, ¿simplemente pasaría MockHttpMessageHandler como la clase messagehandler Httphandler? O cómo lo ha implementado en sus propios proyectos
tjugg
2
Gran respuesta y algo que inicialmente no hubiera sabido. Hace que trabajar con HttpClient no sea tan malo.
Bealer
6
Para las personas que no quieren lidiar con inyectar al cliente pero que aún desean una prueba fácil, es trivial de lograr. Basta con sustituir var client = new HttpClient()con var client = ClientFactory()y configurar un campo internal static Func<HttpClient> ClientFactory = () => new HttpClient();y en el nivel de prueba puede volver a escribir este campo.
Chris Marisic
3
@ChrisMarisic está sugiriendo una forma de ubicación de servicio para reemplazar la inyección. La ubicación del servicio es un anti-patrón bien conocido, por lo que en mi humilde opinión es preferible la inyección.
MarioDS
2
@MarioDS e independientemente, no debería inyectar una instancia de HttpClient en absoluto. Si está decidido a usar la inyección de constructor para esto, entonces debería inyectar HttpClientFactorycomo en Func<HttpClient>. Dado que veo HttpClient como puramente un detalle de implementación y no una dependencia, usaré estática tal como ilustré anteriormente. Estoy completamente de acuerdo con las pruebas que manipulan los componentes internos. Si me importa el puro-ismo, pondré servidores completos y probaré rutas de código en vivo. Usar cualquier tipo de simulacro significa que acepta una aproximación del comportamiento, no el comportamiento real.
Chris Marisic
39

Estoy de acuerdo con algunas de las otras respuestas en que el mejor enfoque es simular HttpMessageHandler en lugar de envolver HttpClient. Esta respuesta es única en el sentido de que todavía inyecta HttpClient, lo que le permite ser un singleton o administrado con inyección de dependencia.

"HttpClient está diseñado para ser instanciado una vez y reutilizado durante la vida de una aplicación". ( Fuente ).

Mocking HttpMessageHandler puede ser un poco complicado porque SendAsync está protegido. Aquí hay un ejemplo completo, usando xunit y 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);
        }
    }
}
Punto cero dos
fuente
1
Realmente me gustó esto. El mockMessageHandler.Protected()fue el asesino. Gracias por este ejemplo. Permite escribir la prueba sin modificar la fuente en absoluto.
tyrion
1
FYI, Moq 4.8 admite burlas fuertemente tipadas de miembros protegidos - github.com/Moq/moq4/wiki/Quickstart
Richard Szalay
2
Esto luce genial. También Moq admite ReturnsAsync, por lo que el código se vería así.ReturnsAsync(new HttpResponseMessage {StatusCode = HttpStatusCode.OK, Content = new StringContent(testContent)})
kord
Gracias @kord, agregué eso a la respuesta
PointZeroTwo
3
¿Hay alguna forma de verificar que "SandAsync" fue llamado con algunos parámetros? Intenté usar ... Protected (). Verify (...), pero parece que no funciona con métodos asíncronos.
Romano
29

Esta es una pregunta común, y yo estaba muy del lado de querer la capacidad de burlarse de HttpClient, pero creo que finalmente me di cuenta de que no debería burlarse de HttpClient. Parece lógico hacerlo, pero creo que las cosas que vemos en las bibliotecas de código abierto nos han lavado el cerebro.

A menudo vemos "Clientes" por ahí que simulamos en nuestro código para que podamos probar de forma aislada, por lo que automáticamente intentamos aplicar el mismo principio a HttpClient. HttpClient realmente hace mucho; puede pensar en él como un administrador para HttpMessageHandler, por lo que no quiere burlarse de eso, y es por eso que todavía no tiene una interfaz. La parte que realmente le interesa para las pruebas unitarias, o incluso para diseñar sus servicios, es HttpMessageHandler, ya que eso es lo que devuelve la respuesta, y puede burlarse de eso.

También vale la pena señalar que probablemente debería comenzar a tratar a HttpClient como algo más importante. Por ejemplo: Mantenga la instalación de nuevos HttpClients al mínimo. Reutilícelos, están diseñados para ser reutilizados y use una tonelada menos recursos si lo hace. Si comienza a tratarlo como algo más importante, se sentirá mucho más incorrecto querer burlarse de él y ahora el controlador de mensajes comenzará a ser lo que está inyectando, no el cliente.

En otras palabras, diseñe sus dependencias alrededor del controlador en lugar del cliente. Aún mejor, "servicios" abstractos que usan HttpClient que le permiten inyectar un controlador y usarlo como su dependencia inyectable. Luego, en sus pruebas, puede falsificar el controlador para controlar la respuesta para configurar sus pruebas.

Envolver HttpClient es una loca pérdida de tiempo.

Actualización: vea el ejemplo de Joshua Dooms. Es exactamente lo que recomiendo.

Sinestésico
fuente
17

Como también se menciona en los comentarios, debe resumir el HttpClientpara no acoplarse a él. He hecho algo similar en el pasado. Intentaré adaptar lo que hice a lo que estás intentando hacer.

Primero mira el HttpClient clase y decida qué funcionalidad proporciona que sería necesaria.

Aquí hay una posibilidad:

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

Nuevamente, como se dijo antes, esto fue para fines particulares. Abstraí completamente la mayoría de las dependencias de cualquier cosa relacionada conHttpClient y me concentré en lo que quería devolver. Debe evaluar cómo desea abstraer elHttpClient para proporcionar solo la funcionalidad necesaria que desea.

Esto ahora le permitirá simular solo lo que se necesita para probar.

Incluso recomendaría eliminar por IHttpHandlercompleto y usar la HttpClientabstracciónIHttpClient . Pero simplemente no estoy eligiendo, ya que puede reemplazar el cuerpo de la interfaz de su controlador con los miembros del cliente abstraído.

IHttpClientLuego, se puede usar una implementación de para envolver / adaptar un objeto real / concreto HttpCliento cualquier otro objeto, que se puede usar para realizar solicitudes HTTP, ya que lo que realmente deseaba era un servicio que brindara esa funcionalidad en relación conHttpClient específico. El uso de la abstracción es un enfoque limpio (Mi opinión) y SÓLIDO y puede hacer que su código sea más fácil de mantener si necesita cambiar el cliente subyacente por otra cosa a medida que cambia el marco.

A continuación, se muestra un fragmento de cómo se podría realizar una implementación.

/// <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 puede ver en el ejemplo anterior, gran parte del trabajo pesado generalmente asociado con el uso HttpClientestá oculto detrás de la abstracción.

Su clase de conexión puede luego inyectarse con el cliente abstraído

public class Connection
{
    private IHttpClient _httpClient;

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

Luego, su prueba puede simular lo que se necesita para su IVU

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
fuente
Esta es la respuesta. Una abstracción anterior HttpClienty un adaptador para crear su instancia específica usando HttpClientFactory. Hacer esto hace que probar la lógica más allá de la solicitud HTTP sea trivial, que es el objetivo aquí.
pimbrouwers
13

Sobre la base de las otras respuestas, sugiero este código, que no tiene dependencias 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
fuente
4
Estás probando efectivamente tu simulacro. El verdadero poder de un simulacro es que puede establecer expectativas y alterar su comportamiento en cada prueba. El hecho de que tenga que implementar algunos HttpMessageHandlerusted mismo lo hace casi imposible, y tiene que hacerlo porque los métodos lo son protected internal.
MarioDS
3
@MarioDS Creo que el punto es que puedes burlarte de la respuesta HTTP para que puedas probar el resto del código. Si inyecta una fábrica que obtiene el HttpClient, en las pruebas puede suministrar este HttpClient.
chris31389
13

Creo que el problema es que lo tienes un poco al revés.

public class AuroraClient : IAuroraClient
{
    private readonly HttpClient _client;

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

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

Si miras la clase anterior, creo que esto es lo que quieres. Microsoft recomienda mantener vivo al cliente para un rendimiento óptimo, por lo que este tipo de estructura le permite hacerlo. Además, HttpMessageHandler es una clase abstracta y, por lo tanto, se puede simular. Su método de prueba se vería así:

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

Esto le permite probar su lógica mientras se burla del comportamiento de HttpClient.

Lo siento chicos, después de escribir esto y probarlo yo mismo, me di cuenta de que no pueden burlarse de los métodos protegidos en HttpMessageHandler. Posteriormente agregué el siguiente código para permitir la inyección de una simulación adecuada.

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

Las pruebas escritas con esto se parecen a lo siguiente:

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

Uno de mis colegas notó que la mayoría de los HttpClientmétodos llamanSendAsync(HttpRequestMessage request, CancellationToken cancellationToken) bajo el capó, que es un método virtual de HttpMessageInvoker:

Entonces, con mucho, la forma más fácil de burlarse HttpClientfue simplemente burlarse de ese método en particular:

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

y su código puede llamar a la mayoría (pero no a todos) de los HttpClientmétodos de clase, incluido un

httpClient.SendAsync(req)

Marque aquí para confirmar https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs

Adán
fuente
1
Sin embargo, esto no funciona para ningún código que llame SendAsync(HttpRequestMessage)directamente. Si puede modificar su código para no usar esta función de conveniencia, entonces burlarse de HttpClient directamente anulando SendAsynces en realidad la solución más limpia que he encontrado.
Dylan Nicholson
8

Una alternativa sería configurar un servidor HTTP stub que devuelva respuestas enlatadas según el patrón que coincida con la URL de la solicitud, lo que significa que prueba las solicitudes HTTP reales, no las simulaciones. Históricamente, esto habría requerido un esfuerzo de desarrollo significativo y habría sido demasiado lento para ser considerado para pruebas unitarias, sin embargo, la biblioteca de OSS WireMock.net es fácil de usar y lo suficientemente rápida como para ejecutarse con muchas pruebas, por lo que puede valer la pena considerarla. La configuración consta de unas pocas líneas 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'}")
   );

Puede encontrar más detalles y orientación sobre el uso de wiremock en las pruebas aquí.

alastairtree
fuente
8

Aquí hay una solución simple que funcionó bien para mí.

Usando la biblioteca simulada de 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>()
);

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

j7nn7k
fuente
También lo he usado con éxito. Prefiero esto a arrastrarme en una dependencia aún más nuget y de hecho también aprendes un poco más sobre lo que está sucediendo bajo el capó. Lo bueno es que la mayoría de los métodos terminan usandoSendAsync todos modos, por lo que no se requiere una configuración adicional.
Steve Pettifer
4

No me convencen muchas de las respuestas.

En primer lugar, imagine que desea realizar una prueba unitaria de un método que utiliza HttpClient. No debe crear HttpClientuna instancia directamente en su implementación. Debe inyectar una fábrica con la responsabilidad de proporcionar una instancia de HttpClientpara usted. De esa manera, puede burlarse más tarde de esa fábrica y devolver lo HttpClientque desee (por ejemplo: un simulacroHttpClient y no la real).

Entonces, tendrías una fábrica como la siguiente:

public interface IHttpClientFactory
{
    HttpClient Create();
}

Y una implementación:

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

Por supuesto, necesitaría registrar esta implementación en su contenedor IoC. Si usa Autofac sería algo como:

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

Ahora tendría una implementación adecuada y comprobable. Imagina que tu método es 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;
        }
    }
}

Ahora la parte de prueba. HttpClientextiende HttpMessageHandler, que es abstracto. Creemos un "simulacro" de HttpMessageHandlerque acepte un delegado para que cuando usemos el simulacro también podamos configurar cada comportamiento para cada prueba.

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

Y ahora, y con la ayuda de Moq (y FluentAssertions, una biblioteca que hace que las pruebas unitarias sean más legibles), tenemos todo lo necesario para realizar pruebas unitarias en nuestro 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, esta prueba es una tontería, y realmente estamos probando nuestro simulacro. Pero se entiende la idea. Debe probar la lógica significativa en función de su implementación, como ..

  • si el estado del código de la respuesta no es 201, ¿debería lanzar una excepción?
  • Si el texto de la respuesta no se puede analizar, ¿qué debería suceder?
  • etc.

El propósito de esta respuesta fue probar algo que usa HttpClient y esta es una forma limpia y agradable de hacerlo.

diegosasw
fuente
4

Unirme a la fiesta un poco tarde, pero me gusta usar wiremock ( https://github.com/WireMock-Net/WireMock.Net ) siempre que sea posible en la integración de un microservicio de dotnet core con dependencias REST descendentes.

Al implementar un TestHttpClientFactory que extiende el IHttpClientFactory, podemos anular el método

HttpClient CreateClient (nombre de cadena)

Entonces, cuando use los clientes nombrados dentro de su aplicación, usted tiene el control de devolver un HttpClient conectado a su wiremock.

Lo bueno de este enfoque es que no está cambiando nada dentro de la aplicación que está probando y habilita las pruebas de integración del curso haciendo una solicitud REST real a su servicio y burlándose del json (o lo que sea) que debería devolver la solicitud descendente real. Esto conduce a pruebas concisas y la menor cantidad de burlas posible en su aplicación.

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

y

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

Dado que HttpClientusa el SendAsyncmétodo para realizar todo HTTP Requests, puede usar el override SendAsyncmétodo y burlarse delHttpClient .

Para esa envoltura creando HttpClienta interface, algo como abajo

public interface IServiceHelper
{
    HttpClient GetClient();
}

Luego, use arriba interfacepara la inyección de dependencia en su servicio, muestra a continuación

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

Ahora, en el proyecto de prueba unitaria, cree una clase de ayuda para burlarse SendAsync. Aquí hay una FakeHttpResponseHandlerclase que inheriting DelegatingHandlerproporcionará una opción para anular el SendAsyncmétodo. Después de anular el SendAsyncmétodo, es necesario configurar una respuesta para cada método HTTP Requestque está llamando SendAsync, para eso, cree un Dictionarywith keyas Uriy valueas HttpResponseMessagepara que siempre que haya un HTTP Requesty si las Uricoincidencias SendAsyncdevuelvan el 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")
        });
    }
}

Cree una nueva implementación para IServiceHelperburlarse del marco o como a continuación. Esta FakeServiceHelperclase la podemos usar para inyectar la FakeHttpResponseHandlerclase de modo que siempre que sea HttpClientcreada por esta classse use en FakeHttpResponseHandler classlugar de la implementación real.

public class FakeServiceHelper : IServiceHelper
{
    private readonly DelegatingHandler delegatingHandler;

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

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

Y en la configuración de prueba FakeHttpResponseHandler classagregando el Uriy esperado HttpResponseMessage. El Uridebe ser el real servicepunto final Uride manera que cuando el overridden SendAsyncmétodo es llamado desde real serviceaplicación que coincidirá con el Uride Dictionaryy responder con el configurado HttpResponseMessage. Después de configurar, inyecte FakeHttpResponseHandler objecta la IServiceHelperimplementación falsa . Luego inyecte el FakeServiceHelper classal servicio real que hará que el servicio real utilice el 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);
    }
}

Enlace de GitHub: que tiene una implementación de muestra

ghosh-arun
fuente
Si bien este código puede resolver la pregunta, incluir una explicación de cómo y por qué esto resuelve el problema realmente ayudaría a mejorar la calidad de su publicación y probablemente resultaría en más votos a favor. Recuerde que está respondiendo la pregunta a los lectores en el futuro, no solo a la persona que pregunta ahora. Por favor, editar su respuesta para agregar explicaciones y dar una indicación de lo que se aplican limitaciones y supuestos.
Богдан Опир
Gracias @ БогданОпир por la explicación actualizada de los comentarios.
ghosh-arun
1

Puede usar la biblioteca RichardSzalay MockHttp que simula el HttpMessageHandler y puede devolver un objeto HttpClient para usarlo durante las pruebas.

GitHub MockHttp

PM> Paquete de instalación RichardSzalay.MockHttp

De la documentación de GitHub

MockHttp define un HttpMessageHandler de reemplazo, el motor que impulsa HttpClient, que proporciona una API de configuración fluida y una respuesta predefinida. La persona que llama (por ejemplo, la capa de servicio de su aplicación) no se da cuenta de su presencia.

Ejemplo de GitHub

 var mockHttp = new MockHttpMessageHandler();

// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
        .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON

// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();

var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;

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

// No network connection required
Console.Write(json); // {'name' : 'Test McGee'}
Justin
fuente
1

Esta es una pregunta antigua, pero siento la necesidad de ampliar las respuestas con una solución que no vi aquí.
Puede falsificar el ensamblaje de Microsoft (System.Net.Http) y luego usar ShinsContext durante la prueba.

  1. En VS 2017, haga clic derecho en el ensamblaje System.Net.Http y seleccione "Agregar ensamblaje falso"
  2. Ponga su código en el método de prueba unitaria bajo ShimsContext.Create () usando. De esta manera, puede aislar el código donde planea falsificar el HttpClient.
  3. Depende de su implementación y prueba, sugeriría implementar todas las acciones deseadas donde llama a un método en HttpClient y desea falsificar el valor devuelto. El uso de ShimHttpClient.AllInstances falsificará su implementación en todas las instancias creadas durante su prueba. Por ejemplo, si desea falsificar el método GetAsync (), haga lo siguiente:

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

Hice algo muy simple, ya que estaba en un entorno 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);

        ...
    }

}

y el simulacro es:

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

Todo lo que necesita es una versión de prueba de la HttpMessageHandlerclase que pasa a HttpClientctor. El punto principal es que su HttpMessageHandlerclase de prueba tendrá un HttpRequestHandlerdelegado que las personas que llaman pueden configurar y simplemente manejar de HttpRequestla manera que quieran.

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

Puede usar una instancia de esta clase para crear una instancia concreta de HttpClient. A través del delegado de HttpRequestHandler, tiene control total sobre las solicitudes http salientes de HttpClient.

Dogu Arslan
fuente
1

Inspirado por la respuesta de PointZeroTwo , aquí hay una muestra que usa NUnit y FakeItEasy .

SystemUnderTest en este ejemplo es la clase que desea probar; no se proporciona contenido de muestra para ella, ¡pero supongo que ya lo tiene!

[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
fuente
¿Qué pasa si quiero pasar un parámetro al método, mencionado en "x.Method.Name" ..?
Shailesh
0

Quizás haya algún código para cambiar en su proyecto actual, pero para proyectos nuevos debería considerar absolutamente usar Flurl.

https://flurl.dev

Es una biblioteca de cliente HTTP para .NET con una interfaz fluida que habilita específicamente la capacidad de prueba del código que la usa para realizar solicitudes HTTP.

Hay muchos ejemplos de código en el sitio web, pero en pocas palabras, lo usa así en su código.

Agregue los usos.

using Flurl;
using Flurl.Http;

Envíe una solicitud de obtención y lea la respuesta.

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

En las pruebas unitarias Flurl actúa como un simulacro que se puede configurar para que se comporte como se desee y también para verificar las llamadas que se realizaron.

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

Después de buscar cuidadosamente, descubrí el mejor enfoque para lograr esto.

    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 habrás notado, he usado paquetes Moq y Moq.Protected.

Amin Mohamed
fuente
0

Para agregar mis 2 centavos. Para simular métodos de solicitud http específicos, ya sea Get o Post. Esto funcionó para mí.

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
fuente