Runtime: ¿Una forma sencilla de simular un método httpClient.GetAsync(...) para pruebas unitarias?

Creado en 4 may. 2015  ·  157Comentarios  ·  Fuente: dotnet/runtime

System.Net.Http ahora se ha subido al repositorio :smile: :tada: :balloon:

Cada vez que he usado esto en algún servicio, funciona muy bien, pero dificulta la prueba unitaria => mis pruebas unitarias no quieren llegar nunca a ese punto final real.

Hace mucho tiempo, le pregunté a @davidfowl qué deberíamos hacer. Espero parafrasear y no citarlo incorrectamente, pero sugirió que necesito falsificar un controlador de mensajes (es decir, HttpClientHandler ), conectarlo, etc.

Como tal, terminé creando una biblioteca de ayuda llamada HttpClient.Helpers para ayudarme a ejecutar mis pruebas unitarias.

Así que esto funciona... pero se siente _muy_ desordenado y... complicado. Estoy seguro de que no soy la primera persona que necesita asegurarse de que las pruebas de mi unidad no hagan una llamada real a un servicio externo.

hay una manera mas facil? Como... ¿no podemos simplemente tener una interfaz IHttpClient y podemos inyectarla en nuestro servicio?

Design Discussion area-System.Net.Http test enhancement

Comentario más útil

Hola, @SidharthNabar : gracias por leer mi problema, también lo aprecio mucho.

Crear una nueva clase de controlador

Acabas de responder mi pregunta :)

Ese es un gran baile para mover también, solo para pedirle a mi código real que no _golpee la red_.

Incluso hice un repositorio HttpClient.Helpers y un paquete nuget... ¡solo para facilitar las pruebas! El escenario es como el camino feliz o una excepción lanzada por el punto final de la red...

Ese es el problema -> ¿podemos no hacer todo esto y ... en su lugar, solo un método simulado?

Intentaré explicarlo a través del código.

Objetivo: Descargar algo de las interwebs.

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

Veamos dos ejemplos:

Dada una interfaz (si existiera):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

Mi código ahora puede verse así...

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

y la clase de prueba

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

¡Hurra! hecho.

Ok, ahora con la forma actual...

  1. crear una nueva clase de Manejador - ¡sí, una clase!
  2. Inyectar el controlador en el servicio.
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

Pero el dolor es ahora que tengo que hacer una clase. Ahora me has hecho daño.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

Y esta clase comienza bastante simple. Hasta que tenga varios GetAsync calls (¿debo proporcionar varias instancias de controlador?) -o- varios httpClients en un solo servicio. Además, ¿hacemos Dispose del controlador o lo reutilizamos si estamos haciendo varias llamadas en el mismo bloque de lógica (que es una opción ctor)?

p.ej.

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

esto se puede hacer mucho más fácil con esto ..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

limpio limpio limpio :)

Luego, está el siguiente bit: Descubrimiento

Cuando comencé a usar MS.Net.Http.HttpClient la API era bastante obvia :+1: Ok, obtén una cadena y hazlo de forma asíncrona... simple...

... pero luego llegué a la prueba ... ¿y ahora se supone que debo aprender sobre HttpClientHandlers ? ¿Por qué? Siento que todo esto debería estar oculto bajo las sábanas y no debería tener que preocuparme por todos estos detalles de implementación. ¡Es demasiado! Estás diciendo que debería empezar a mirar dentro de la caja y aprender algo de la plomería... lo cual duele :cry:

Todo está haciendo que esto sea más complejo de lo que debería ser, en mi opinión.


Ayúdame Microsoft, eres mi única esperanza.

Todos 157 comentarios

HttpClient no es más que una capa de abstracción sobre otra biblioteca HTTP. Uno de sus propósitos principales es permitirle reemplazar el comportamiento (implementación), incluida, entre otras, la capacidad de crear pruebas unitarias deterministas.

En otros trabajos, HttpClient sirve tanto como objeto "real" como "simulado", y HttpMessageHandler es lo que selecciona para satisfacer las necesidades de su código.

Pero si parece que se requiere mucho trabajo para configurar el controlador de mensajes cuando (parece) esto podría manejarse con una buena interfaz. ¿Estoy totalmente malinterpretando la solución?

@PureKrome : gracias por traer esto a discusión. ¿Puede dar más detalles sobre lo que quiere decir con "se requiere tanto trabajo para configurar el controlador de mensajes"?

Una forma de probar la unidad HttpClient sin acceder a la red es:

  1. Cree una nueva clase de controlador (por ejemplo, FooHandler) que derive de HttpMessageHandler
  2. Implemente el método SendAsync según sus requisitos: no acceda a la red/registre la solicitud-respuesta/etc.
  3. Pase una instancia de este FooHandler al constructor de HttpClient:
    manejador de variables = nuevo FooHandler();
    var cliente = nuevo HttpClient (controlador);

Su objeto HttpClient luego usará su controlador en lugar del HttpClientHandler incorporado.

Gracias,
sid

Hola, @SidharthNabar : gracias por leer mi problema, también lo aprecio mucho.

Crear una nueva clase de controlador

Acabas de responder mi pregunta :)

Ese es un gran baile para mover también, solo para pedirle a mi código real que no _golpee la red_.

Incluso hice un repositorio HttpClient.Helpers y un paquete nuget... ¡solo para facilitar las pruebas! El escenario es como el camino feliz o una excepción lanzada por el punto final de la red...

Ese es el problema -> ¿podemos no hacer todo esto y ... en su lugar, solo un método simulado?

Intentaré explicarlo a través del código.

Objetivo: Descargar algo de las interwebs.

public async Foo GetSomethingFromTheInternetAsync()
{
    ....
    using (var httpClient = new HttpClient())
    {
        html = await httpClient.GetStringAsync("http://www.google.com.au");
    }
    ....
}

Veamos dos ejemplos:

Dada una interfaz (si existiera):

public interface IHttpClient
{
    Task<string> GetStringAsync(string requestUri);    
}

Mi código ahora puede verse así...

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient()) // <-- CHANGE dotnet/corefx#1
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

y la clase de prueba

public async Task GivenAValidEndPoint_GetSomethingFromTheInternetAsync_ReturnsSomeData()
{
    // Create the Mock : FakeItEasy > Moq.
    var httpClient = A.Fake<IHttpClient>();

    // Define what the mock returns.
    A.CallTo(()=>httpClient.GetStringAsync("http://www.google.com.au")).Returns("some html here");

    // Inject the mock.
    var service = new SomeService(httpClient);
    ...
}

¡Hurra! hecho.

Ok, ahora con la forma actual...

  1. crear una nueva clase de Manejador - ¡sí, una clase!
  2. Inyectar el controlador en el servicio.
public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null 
                                    ? new HttpClient()
                                    : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

Pero el dolor es ahora que tengo que hacer una clase. Ahora me has hecho daño.

public class FakeHttpMessageHandler : HttpClientHandler
{
 ...
}

Y esta clase comienza bastante simple. Hasta que tenga varios GetAsync calls (¿debo proporcionar varias instancias de controlador?) -o- varios httpClients en un solo servicio. Además, ¿hacemos Dispose del controlador o lo reutilizamos si estamos haciendo varias llamadas en el mismo bloque de lógica (que es una opción ctor)?

p.ej.

public async Foo GetSomethingFromTheInternetAsync()
{
    string[] results;
    using (var httpClient = new HttpClient())
    {
        var task1 = httpClient.GetStringAsync("http://www.google.com.au");
        var task2 = httpClient.GetStringAsync("http://www.microsoft.com.au");

        results = Task.WhenAll(task1, task2).Result;
    }
    ....
}

esto se puede hacer mucho más fácil con esto ..

var httpClient = A.Fake<IHttpClient>();
A.CallTo(() = >httpClient.GetStringAsync("http://www.google.com.au"))
    .Returns("gooz was here");
A.CallTo(() = >httpClient.GetStringAsync("http://www.microsoft.com.au"))
    .Returns("ms was here");

limpio limpio limpio :)

Luego, está el siguiente bit: Descubrimiento

Cuando comencé a usar MS.Net.Http.HttpClient la API era bastante obvia :+1: Ok, obtén una cadena y hazlo de forma asíncrona... simple...

... pero luego llegué a la prueba ... ¿y ahora se supone que debo aprender sobre HttpClientHandlers ? ¿Por qué? Siento que todo esto debería estar oculto bajo las sábanas y no debería tener que preocuparme por todos estos detalles de implementación. ¡Es demasiado! Estás diciendo que debería empezar a mirar dentro de la caja y aprender algo de la plomería... lo cual duele :cry:

Todo está haciendo que esto sea más complejo de lo que debería ser, en mi opinión.


Ayúdame Microsoft, eres mi única esperanza.

A mí también me encantaría ver una manera fácil y sencilla de probar varias cosas que usan HttpClient. :+1:

Gracias por la respuesta detallada y los fragmentos de código, eso realmente ayuda.

Primero, noto que está creando nuevas instancias de HttpClient para enviar cada solicitud; ese no es el patrón de diseño previsto para HttpClient. Crear una instancia de HttpClient y reutilizarla para todas sus solicitudes ayuda a optimizar la agrupación de conexiones y la administración de la memoria. Considere reutilizar una única instancia de HttpClient. Una vez que haga esto, puede insertar el controlador falso en una sola instancia de HttpClient y listo.

Segundo - tienes razón. El diseño de la API de HttpClient no se presta a un mecanismo de prueba puramente basado en la interfaz. Estamos considerando agregar un método estático/patrón de fábrica en el futuro que le permitirá modificar el comportamiento de todas las instancias de HttpClient creadas a partir de esa fábrica o después de ese método. Sin embargo, aún no nos hemos decidido por un diseño para esto. Pero el problema clave permanecerá: deberá definir un controlador falso e insertarlo debajo del objeto HttpClient.

@ericstj - ¿pensamientos sobre esto?

Gracias,
Sid.

@SidharthNabar , ¿por qué usted o su equipo dudan tanto en ofrecer una interfaz para esta clase? Esperaba que el hecho de que un desarrollador tenga que _aprender_ sobre los controladores y luego tener que _crear_ clases de controladores falsas sea un punto de dolor suficiente para justificar (o al menos, resaltar) la necesidad de una interfaz.

Sí, no veo por qué una interfaz sería algo malo. Sería mucho más fácil probar HttpClient.

Una de las principales razones por las que evitamos las interfaces en el marco es que no funcionan bien. La gente siempre puede crear uno propio si lo necesita y eso no se romperá cuando la próxima versión del marco necesite agregar un nuevo miembro. @KrzysztofCwalina o @terrajobst podrían compartir más sobre la historia/los detalles de esta guía de diseño. Este tema en particular es un debate casi religioso: soy demasiado pragmático para participar en eso.

HttpClient tiene muchas opciones para la comprobación de unidades. No está sellado y la mayoría de sus miembros son virtuales. Tiene una clase base que se puede usar para simplificar la abstracción, así como un punto de extensión cuidadosamente diseñado en HttpMessageHandler como señala @sharwell . En mi opinión, es una API bastante bien diseñada, gracias a @HenrikFrystykNielsen.

:+1: una interfaz

:wave: @ericstj Muchas gracias por participar :+1: :cake:

Una de las principales razones por las que evitamos las interfaces en el marco es que no funcionan bien.

Sí, gran punto.

Este tema en particular es un debate casi religioso.

sí ... punto bien tomado en eso.

HttpClient tiene muchas opciones para la capacidad de prueba de la unidad

¿Oh? Estoy luchando con esto :blush: de ahí el motivo de este problema :blush:

No está sellado y la mayoría de sus miembros son virtuales.

¿Los miembros son virtuales? ¡Maldita sea, me lo perdí por completo! Si son virtuales, los marcos de trabajo simulados pueden burlarse de esos miembros :+1: ¡y no necesitamos una interfaz!

Echemos un vistazo a HttpClient.cs ...

public Task<HttpResponseMessage> GetAsync(string requestUri) { .. }
public Task<HttpResponseMessage> GetAsync(Uri requestUri) { .. }

mmm. esos no son virtuales... vamos a buscar en el archivo... eh... no se encontró la palabra clave virtual . Entonces, ¿quieres decir que _otros_ miembros de _otras clases relacionadas_ son virtuales? Si es así... entonces volvemos al problema que estoy planteando: ahora tenemos que mirar debajo del capó para ver qué está haciendo GetAsync para que sepamos qué crear/conectar/etc...

Supongo que no estoy entendiendo algo realmente básico, aquí ¯(°_°)/¯ ?

EDITAR: ¿tal vez esos métodos pueden ser virtuales? ¡Puedo relaciones públicas!

SendAsync es virtual y casi todas las demás capas de API además de eso. 'La mayoría' era incorrecto, mi memoria me falla allí. Mi impresión fue que la mayoría eran efectivamente virtuales ya que se construyen sobre un miembro virtual. Por lo general, no hacemos que las cosas sean virtuales si son sobrecargas en cascada. Hay una sobrecarga más específica de SendAsync que no es virtual, que podría solucionarse.

¡Ay! Gotcha :blush: Así que todos esos métodos terminan llamando a SendAsync .. que hace todo el trabajo pesado. Así que esto todavía significa que tenemos un problema de descubrimiento aquí... pero dejemos eso de lado (eso es obstinado).

¿Cómo nos burlaríamos SendAsync dando esta muestra básica...
Install-Package Microsoft.Net.Http
Install-Package xUnit

public class SomeService
{
    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }
}

public class Facts
{
    [Fact]
    public async Task GivenAValidUrl_GetSomeData_ReturnsSomeData()
    {
        // Arrange.
        var service = new SomeService();

        // Act.
        var result = await service.GetSomeData("http://www.google.com");

        // Assert.
        Assert.True(result.Length > 0);
    }
}

Una de las principales razones por las que evitamos las interfaces en el marco es que no funcionan bien. La gente siempre puede crear uno propio si lo necesita y eso no se romperá cuando la próxima versión del marco necesite agregar un nuevo miembro.

Puedo entender totalmente esta forma de pensar con .NET Framework completo.

Sin embargo, .NET Core se divide en muchos paquetes pequeños, cada uno con una versión independiente. Utilice el control de versiones semántico, actualice la versión principal cuando haya un cambio importante y listo. Las únicas personas afectadas serán las que actualicen explícitamente sus paquetes a la nueva versión principal, sabiendo que hay cambios importantes documentados.

No estoy defendiendo que deba romper todas las interfaces todos los días: idealmente, los cambios importantes deberían agruparse en una nueva versión principal.

Me parece triste paralizar las API por motivos de compatibilidad en el futuro. ¿No era uno de los objetivos de .NET Core iterar más rápido, ya que ya no tiene que preocuparse por una actualización sutil de .NET que rompe miles de aplicaciones ya instaladas?

Mis dos centavos.

@MrJul :+1:
El control de versiones no debería ser un problema. Por eso existen los números de versión :)

El control de versiones sigue siendo un gran problema. Agregar un miembro a una interfaz es un cambio importante. Para las bibliotecas principales como esta que se encuentran en la bandeja de entrada del escritorio, queremos recuperar las funciones del escritorio en versiones futuras. Si bifurcamos, significa que la gente no puede escribir código portátil que se ejecute en ambos lugares. Para obtener más información sobre cambios importantes, consulte: https://github.com/dotnet/corefx/wiki/Breaking-Changes.

Creo que esta es una buena discusión, pero como mencioné antes, no tengo muchos detalles en el argumento sobre las interfaces frente a las clases abstractas. ¿Quizás este es un tema para llevar a la próxima reunión de diseño?

No estoy muy familiarizado con la biblioteca de prueba que está utilizando, pero lo que haría es proporcionar un gancho para permitir que las pruebas configuren la instancia del cliente y luego creen un objeto simulado que se comporte como yo quiero. El objeto simulado podría ser mutable para permitir cierta reutilización.

Si tiene un cambio compatible específico que le gustaría sugerir a HttpClient que mejore la capacidad de prueba de la unidad, propóngalo y @SidharthNabar y su equipo pueden considerarlo.

Si la API no se puede burlar, deberíamos arreglarlo. Pero no tiene nada que ver con interfaces vs clases. La interfaz no es diferente de la clase abstracta pura desde la perspectiva de la burlabilidad, para todos los propósitos prácticos.

@KrzysztofCwalina , ¡usted, señor, dio en el clavo! resumen perfecto!

Supongo que tomaré el lado menos popular de este argumento ya que personalmente no creo que sea necesaria una interfaz. Como ya se ha señalado, HttpClient ya es la abstracción. El comportamiento realmente proviene del HttpMessageHandler . Sí, no es simulable/falsificable usando los enfoques a los que nos hemos acostumbrado con marcos como Moq o FakeItEasy, pero no es necesario que lo sea; esa no es la _única_ manera que hay de hacer las cosas. Sin embargo, la API todavía es perfectamente comprobable por diseño.

Entonces, abordemos el problema "¿Necesito crear mi propio HttpMessageHandler ?". No claro que no. No todos escribimos nuestras propias bibliotecas burlonas. @PureKrome ya ha mostrado su biblioteca HttpClient.Helpers . Personalmente no he usado ese, pero lo revisaré. He estado usando la biblioteca MockHttp de @richardszalay , que me parece fantástica. Si alguna vez ha trabajado con $httpBackend de AngularJS, MockHttp está adoptando exactamente el mismo enfoque.

En cuanto a la dependencia, su clase de servicio debe permitir que se inyecte un HttpClient y, obviamente, puede proporcionar el valor predeterminado razonable de new HttpClient() . Si necesita poder crear instancias, tome un Func<HttpClient> o, si no es fanático de Func<> por cualquier motivo, diseñe su propia abstracción IHttpClientFactory y un la implementación predeterminada de eso, nuevamente, solo devolvería new HttpClient() .

Para terminar, encuentro esta API perfectamente comprobable en su forma actual y no veo la necesidad de una interfaz. Sí, requiere un enfoque diferente, pero las bibliotecas antes mencionadas ya existen para ayudarnos con este estilo diferente de prueba de manera perfectamente lógica e intuitiva. Si queremos más funcionalidad/simplicidad, contribuyamos a esas bibliotecas para mejorarlas.

Personalmente, una interfaz o métodos virtuales no me importan mucho. Pero vamos, perfectly testable es demasiado :)

@luisrudge Bueno, ¿puede dar un escenario que no se pueda probar usando el estilo de prueba del controlador de mensajes que habilita algo como MockHttp? Tal vez eso ayudaría a hacer el caso.

Todavía no he encontrado nada que no pueda codificar, pero tal vez me estoy perdiendo algunos escenarios más esotéricos que no se pueden cubrir. Incluso entonces, podría ser una característica faltante de esa biblioteca que alguien podría contribuir.

Por ahora, mantengo la opinión de que es "perfectamente comprobable" como dependencia, solo que no en la forma en que los desarrolladores de .NET están acostumbrados.

Es comprobable, estoy de acuerdo. pero sigue siendo un dolor. Si tiene que usar una biblioteca externa que lo ayude a hacerlo, no es perfectamente comprobable. Pero supongo que eso es demasiado subjetivo para tener una discusión al respecto.

Se podría argumentar que aspnet mvc 5 es perfectamente comprobable, solo tiene que escribir 50LOC para burlarse de todo lo que necesita un controlador. En mi humilde opinión, ese es el mismo caso con HttpClient. ¿Es comprobable? Si. ¿Es fácil de probar? No.

@luisrudge Sí, estoy de acuerdo, es subjetivo y entiendo completamente el deseo de interfaz/virtuales. Solo estoy tratando de asegurarme de que cualquiera que venga y lea este hilo al menos obtenga alguna educación sobre el hecho de que esta API _puede_ aprovecharse en una base de código de una manera muy comprobable sin introducir todas sus propias abstracciones alrededor HttpClient para llegar allí.

si tiene que usar una biblioteca externa que lo ayude a hacerlo, no es perfectamente comprobable

Bueno, todos estamos usando una biblioteca u otra para burlarnos/falsificar * y, es cierto, no podemos usar _esa_ para probar este estilo de API, pero no creo que eso signifique que sea menos comprobable que un enfoque basado en la interfaz solo porque tengo que traer otra biblioteca.

_ * ¡Al menos espero que no!_

@drub0y de mi OP He declarado que la biblioteca es comprobable, pero es solo un dolor real en comparación (con lo que creo apasionadamente) con lo que _podría_ ser. En mi opinión, @luisrudge lo explicó perfectamente:

Se podría argumentar que aspnet mvc 5 es perfectamente comprobable, solo tiene que escribir 50LOC para burlarse de todo lo que necesita un controlador

Este repositorio es una parte importante de un _gran_ número de desarrolladores. Entonces, la táctica predeterminada (y comprensible) es ser _muy_ cauteloso con este repositorio. Claro, lo entiendo totalmente.

Me gustaría creer que este repositorio todavía está en condiciones de modificarse (con respecto a la API) antes de que se convierta en RTW. Los cambios futuros de repente se vuelven _realmente_ difíciles de hacer, incluye la _percepción_ para cambiar.

Entonces, con la API actual, ¿se puede probar? Sí. ¿Pasa la prueba Dark Matter Developer? Yo personalmente no lo creo.

La prueba de fuego, en mi opinión, es esta: ¿puede un joe regular elegir uno de los marcos de prueba comunes/populares + marcos de burla y burlarse de cualquiera de los métodos principales en esta API? Ahora mismo, no. Entonces, el desarrollador debe dejar de hacer lo que está haciendo y comenzar a aprender sobre la _implementación_ de esta biblioteca.

Como ya se ha señalado, HttpClient ya es la abstracción. El comportamiento realmente proviene de HttpMessageHandler.

Este es mi punto: ¿por qué nos haces pasar ciclos descubriendo esto cuando el propósito de la biblioteca es abstraer toda esa magia... solo para decir "Ok... hemos hecho algo de magia pero ahora quieres burlarte nuestra magia... Lo siento, ahora vas a tener que subirte las mangas, levantar el techo de la biblioteca y empezar a cavar dentro, etc.

Se siente tan... enrevesado.

Estamos en posición de hacerle la vida más fácil a tantas personas y de seguir volviendo a la posición defensiva de: "Se puede hacer, pero... lea las Preguntas Frecuentes/Código".

Aquí hay otro ángulo para abordar este problema: Dé 2 ejemplos a desarrolladores aleatorios que no sean de MS... joe ciudadanos desarrolladores que saben cómo usar xUnit y Moq/FakeItEasy/InsertTheHipMockingFrameworkThisYear.

  • Primera API que usa una interfaz/virtual/lo que sea... pero el método puede ser burlado.
  • Segunda API que es solo un método público y para simular el _punto de integración_ ... tienen que leer el código.

Se reduce a eso, en mi opinión.

Vea qué desarrollador puede resolver ese problema _y_ manténgase feliz.

Si la API no se puede burlar, deberíamos arreglarlo.

En este momento no es IMO, pero hay formas de evitar esto con éxito (nuevamente, eso es obstinado, lo reconoceré)

Pero no tiene nada que ver con interfaces vs clases. La interfaz no es diferente de la clase abstracta pura desde la perspectiva de la burlabilidad, para todos los propósitos prácticos.

Exactamente, ese es un detalle de implementación. El objetivo es poder usar un marco simulado probado en batalla y listo.

@luisrudge si tiene que usar una biblioteca externa que lo ayude a hacer eso, no es perfectamente comprobable
@drub0y Bueno, todos estamos usando una biblioteca u otra para burlarnos/falsificar

(Espero haber entendido la última cita/párrafo...) No... tranquilo. Lo que @luisrudge estaba diciendo es: "Tenemos una herramienta para probar. Una segunda herramienta para burlarse. Hasta ahora, esas herramientas genéricas/generales no están vinculadas a nada. Pero... ahora quieres que descargue una tercera herramienta que es _específica_ para burlarse de una API/servicio _específico_ en nuestro código porque esa API/servicio específico que estamos usando, no fue diseñado para ser probado correctamente?". eso es un poco rico :(

En cuanto a la dependencia, su clase de servicio debería permitir que se inyecte un HttpClient y, obviamente, puede proporcionar el valor predeterminado razonable de new HttpClient ()

¡Completamente de acuerdo! Entonces, ¿puede refactorizar el código de muestra anterior para mostrarnos qué tan fácil debería/puede ser esto? Hay muchas maneras de despellejar a un gato... entonces, ¿cuál es una manera simple Y fácil? Muéstranos el código.

Yo empezare. Pido disculpas a @KrzysztofCwalina por usar una interfaz, pero esto es solo para iniciar mi prueba de fuego.

public interface IHttpClient
{
    Task<string> GetStringAsync(string url);
}

public class SomeService
{
    private IHttpClient _httpClient;

    // Injected.
    public SomeService(IHttpClient httpClient) { .. }

    public async Task<string> GetSomeData(string url)
    {
        string result;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            result = await httpClient.GetStringAsync(url);
        }

        return result;
    }    
}

Parece que todo lo que necesita @PureKrome es una actualización de la documentación que explique qué métodos simular/anular para personalizar el comportamiento de la API durante las pruebas.

@sharwell ese no es el caso en absoluto. Cuando pruebo cosas, no quiero ejecutar todo el código de httpclient:
Mira el método SendAsync .
Es una locura. Lo que estás sugiriendo es que cada desarrollador debería conocer los aspectos internos de la clase, abrir github, ver el código y ese tipo de cosas para poder probarlo. Quiero que el desarrollador se burle de los GetStringAsync y lo haga.

@luisrudge En realidad, mi sugerencia sería evitar burlarse de esta biblioteca por completo. Dado que ya tiene una capa de abstracción limpia con una implementación reemplazable y desacoplada, no es necesario simular más. Burlarse de esta biblioteca tiene las siguientes desventajas adicionales:

  1. Mayor carga para los desarrolladores que crean pruebas (mantienen los simulacros), lo que a su vez aumenta los costos de desarrollo
  2. Mayor probabilidad de que sus pruebas no detecten ciertos comportamientos problemáticos, como reutilizar el flujo de contenido asociado con un mensaje (el envío de un mensaje da como resultado una llamada a Dispose en el flujo de contenido, pero la mayoría de los simulacros lo ignorarán)
  3. Acoplamiento innecesariamente más estrecho de la aplicación a una capa de abstracción particular, lo que aumenta los costos de desarrollo asociados con la evaluación de alternativas a HttpClient

La simulación es una estrategia de prueba dirigida a bases de código entrelazadas que son difíciles de probar a pequeña escala. Si bien el uso de burlas se correlaciona con mayores costos de desarrollo, la _necesidad_ de burlas se correlaciona con una mayor cantidad de acoplamiento en el código. Esto significa que burlarse de sí mismo es un olor a código. Si puede proporcionar la misma cobertura de prueba de entrada-salida en su API y dentro de ella sin utilizar la simulación, se beneficiará básicamente de todos los aspectos del desarrollo.

Dado que ya tiene una capa de abstracción limpia con una implementación reemplazable y desacoplada, no es necesario simular más

Con respeto, si la opción es tener que escribir su propio controlador de mensajes falsos, lo cual no es obvio sin profundizar en el código, entonces esa es una barrera muy alta que será muy frustrante para muchos desarrolladores.

Estoy de acuerdo con el comentario anterior de @luisrudge . _Técnicamente_ MVC5 fue comprobable, pero Dios mío, fue un dolor en el trasero y una inmensa fuente de tirones de pelo.

@sharwell ¿Está diciendo que burlarse IHttpClient es una carga e implementar un controlador de mensajes falso (como este no lo es? No puedo estar de acuerdo con eso, lo siento.

@luisrudge Para casos simples de prueba GetStringAsync , no es tan difícil burlarse del controlador. Usando Moq + HttpClient listos para usar, todo lo que necesita es esto:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) }));
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

Si eso es demasiado, puede definir una clase auxiliar:

internal static class MockHttpClientHandlerExtensions
{
    public static void SetupGetStringAsync(this Mock<HttpClientHandler> mockHandler, Uri requestUri, string response)
    {
        mockHandler.Protected()
            .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
            .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response) }));
    }
}

Usando la clase auxiliar todo lo que necesitas es esto:

Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";

Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.SetupGetStringAsync(requestUri, expectedResponse);
HttpClient httpClient = new HttpClient(mockHandler.Object);
string result = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false);
Assert.AreEqual(expectedResponse, result);

Así que para comparar las dos versiones:
_Descargo de responsabilidad_: este es un código de navegador no probado. Además, no he usado Moq en mucho tiempo.

Actual

public class SomeService(HttpClientHandler httpClientHandler= null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _handler == null
                                  ? new HttpClient
                                  : new HttpClient(handler))
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit test...
// Arrange.
Uri requestUri = new Uri("http://google.com");
string expectedResponse = "Response text";
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);
var someService = new SomeService(mockHandler.Object); // Injected...

// Act.
string result = await someService.GetSomethingFromTheInternetAsync();

// Assert.
Assert.AreEqual(expectedResponse, result);
httpClient.Verify(x => x.GetSomethingFromTheInternetAsync(), Times.Once);

en lugar de ofrecer una forma de simular el método API directamente.

De otra manera

public class SomeService(IHttpClient httpClient = null)
{
    public async Foo GetSomethingFromTheInternetAsync()
    {
        ....
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            html = await httpClient.GetStringAsync("http://www.google.com.au");
        }
        ....
    } 
}

// Unit tests...
// Arrange.
var response = new Foo();
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);
var service = new SomeService(httpClient.Object); // Injected.

// Act.
var result = await service.GetSomethingFromTheInternetAsync();

// Assert.
Assert.Equals("something", foo.Something);
httpClient.Verify(x => x.GetStringAsync(It.IsAny<string>()), Times.Once);

destilando eso, se trata principalmente de esto (excluyendo la ligera diferencia en SomeService )...

var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(expectedResponse) };
Mock<HttpClientHandler> mockHandler = new Mock<HttpClientHandler>();
mockHandler.Protected()
    .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(message => message.RequestUri == requestUri), ItExpr.IsAny<CancellationToken>())
    .ReturnsAsync(responseMessage);
HttpClient httpClient = new HttpClient(mockHandler.Object);

contra esto...

var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.GetStringAsync(It.IsAny<string>))
    .ReturnsAsync(response);

¿El código _aproximadamente_ se ve bien para ambos? (ambos tienen que ser bastante precisos, antes de que podamos compararlos).

@sharwell , ¿entonces tengo que ejecutar mi código a través de todo el código HttpClient.cs para ejecutar mis pruebas? ¿Cómo es eso menos acoplado con HttpClient?

Usamos mucho HttpClient en algunos de nuestros sistemas y no lo llamaría una pieza de tecnología perfectamente comprobable. Realmente me gusta la biblioteca, pero tener algún tipo de interfaz o cualquier otra mejora para las pruebas sería una gran mejora en mi opinión.

Felicitaciones a @PureKrome por su biblioteca, pero sería mejor si no fuera necesario :)

Acabo de leer todos los comentarios y aprendí mucho, pero tengo una pregunta: ¿por qué necesita burlarse de HttpClient en sus pruebas?

En los escenarios más comunes, la funcionalidad proporcionada por HttpClient ya estará incluida en alguna clase como HtmlDownloader.DownloadFile() . ¿Por qué no te burlas de esta clase en lugar de sus cañerías?

Otro escenario posible que requeriría tal burla es cuando tiene que analizar una cadena descargada y esta pieza de código requiere pruebas para múltiples salidas diferentes. Incluso en este caso, esta funcionalidad (análisis) podría extraerse a una clase/método y probarse sin simulacros.

No digo que la capacidad de prueba no se pueda mejorar, ni tampoco implica que no deba serlo. Solo quiero entender para aprender algunas cosas nuevas.

@tucaz Imaginemos que tengo un sitio web que devuelve información sobre ti. Su edad, nombre, cumpleaños, etc. También tiene acciones en algunas empresas.

¿Ha dicho que posee 100 unidades de acciones AAA y 200 unidades de acciones BBB? Por lo tanto, cuando mostramos los detalles de su cuenta, _deberíamos_ mostrar cuánto dinero valen sus acciones.

Para hacer esto, necesitamos obtener los precios actuales de las acciones de _otro_ sistema. Entonces, para hacer esto, necesitamos recuperar las existencias con un servicio, ¿verdad? Vamos a esto...

_Descargo de responsabilidad: código del navegador..._

public class StockService : IStockService
{
    private readonly IHttpClient _httpClient;

    public StockService(IHttpClient httpClient)
    {
        _httpClient = httpClient; // Optional.
    }

    public async Task<IEnumerable<StockResults>> GetStocksAsync(IEnumerable<string> stockCodes)
    {
        var queryString = ConvertStockCodeListIntoQuerystring();
        string jsonResult;
        using (var httpClient = _httpClient ?? new HttpClient())
        {
            jsonResult = await httpClient.GetStringAsync("http:\\www.stocksOnline.com\stockResults?" + queryString);
        }

        return JsonConvert.DeserializeObject<StockResult>(jsonResult);
    }
}

Bien, aquí tenemos un servicio simple que dice: "_Ve a buscarme los precios de las acciones AAA y BBB_";

Entonces, necesito probar esto:

  • Dados algunos nombres de acciones legítimos
  • Dados algunos nombres de acciones inválidos/inexistentes
  • httpClient lanza una excepción (internet explotó mientras esperaba una respuesta)

y ahora puedo probar fácilmente esos escenarios. Me doy cuenta de que cuando httpClient explota (produce una excepción)... este servicio explota... así que tal vez deba agregar algo de registro y devolver un valor nulo. donno .. pero al menos puedo pensar en el problema y luego manejarlo.

Por supuesto, 100% no quiero realmente acceder al servicio web real durante mis pruebas de unidad normales. Al inyectar el simulacro, puedo usar eso. De lo contrario, puedo crear una nueva instancia y usar eso.

Así que estoy tratando de establecer que mi sugerencia es muchísimo más fácil (de leer/mantener/etc.) que el status quo actual.

En los escenarios más comunes, la funcionalidad proporcionada por HttpClient ya estará incluida en alguna clase, como HtmlDownloader.DownloadFile().

¿Por qué deberíamos envolver HttpClient? Es _ya_ una abstracción sobre todos los http :destellos: debajo. ¡Es una gran biblioteca! Personalmente, no veo lo que eso lograría.

@PureKrome , su ejemplo de uso es exactamente lo que quiero decir con envolver la funcionalidad en una clase de envoltorio.

No estoy seguro de poder entender lo que está tratando de probar, pero para mí parece que está probando el servicio web subyacente cuando lo que realmente necesita afirmar es que quien lo esté consumiendo podrá manejar lo que sea que regrese de GetStockAsync método.

Por favor considere lo siguiente:

private IStockService stockService;

[Setup]
public void Setup()
{
    stockService = A.Fake<IStockService>();

    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker1" }))
        .Returns(new StockResults[] { new StockResults { Price = 100m, Ticker = "Ticker1", Status = "OK" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker2" }))
        .Returns(new StockResults[] { new StockResults { Price = 0m, Ticker = "Ticker2", Status = "NotFound" }});
    A.CallTo(() => stockService.GetStocksAsync(new [] { "Ticker3" }))
        .Throws(() => new InvalidOperationException("Some weird message"));
}

[Test]
public async void Get_Total_Stock_Quotes()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker1" });

    Assert.IsNotNull(total);
    Assert.IsGreaterThan(0, total.Sum(x => x.Price);
}

[Test]
public async void Hints_When_Ticker_Not_Found()
{
    var stockService = A.Fake<IStockService>();

    var total = await stockService.GetStockAsync(new [] { "Ticker2" });

    Assert.IsNotNull(total);
    Assert.AnyIs(x => x.Status == "NotFound");
}

[Test]
public async void Throws_InvalidOperationException_When_Error()
{
    var stockService = A.Fake<IStockService>();

    Assert.Throws(() => await stockService.GetStockAsync(new [] { "Ticker3" }), typeof(InvalidOperationException));
}

Le propongo que aumente su abstracción un nivel y se ocupe de la lógica comercial real, ya que no hay nada que deba probarse con respecto al comportamiento del método GetStockAsync .

El único escenario en el que puedo pensar en la necesidad de simular el HttpClient es cuando necesita probar alguna lógica de reintento dependiendo de la respuesta recibida del servidor o algo así. En este caso, creará una pieza de código más complicada que probablemente se extraerá en una clase HttpClientWithRetryLogic y no se distribuirá en todos los métodos que codifique que usen la clase HttpClient .

Si este es el caso, puede usar el controlador de mensajes como se sugiere o crear una clase para envolver toda esta lógica de reintento porque HttpClient será una parte muy pequeña de lo importante que hace su clase.

@tucaz ¿No acaba de probar que su falsificación devuelve lo que configuró, probando efectivamente la biblioteca de simulación? @PureKrome quiere probar la implementación concreta StockService .

El nivel de abstracción recientemente introducido debe envolver las llamadas HttpClient para poder probar correctamente StockService . Lo más probable es que el contenedor simplemente delegue cada llamada a HttpClient y agregue una complejidad innecesaria a la aplicación.

Siempre puede agregar una nueva capa de abstracción para ocultar tipos no simulables. Lo que se critica en este hilo es que es posible burlarse HttpClient , pero probablemente debería ser más fácil / en línea con lo que generalmente se hace con otras bibliotecas.

@MrJul tienes razón. Es por eso que dije que lo que se probó con su ejemplo es la implementación del servicio web en sí y por qué propuse (sin ejemplos de código. Mi error) que se burle del IStockService para poder afirmar que el código que lo consume es capaz de maneje lo que devuelva GetStockAsync .

Eso es lo que creo que debería probarse en primer lugar. No el marco de burla (como lo hice yo) o la implementación del servicio web (como @PureKrome quiere hacer).

Al final del día, puedo entender que sería bueno tener una interfaz IHttpClient , pero no veo ninguna utilidad en este tipo de escenarios, ya que, en mi opinión, lo que debe probarse es cómo los consumidores del servicio abstraen (y, por lo tanto, el servicio web) puede manejar su devolución de manera adecuada.

@tucaz no . Su prueba es incorrecta. Estás probando la biblioteca de simulación. Usando el ejemplo de @PureKrome , uno querría probar tres cosas:

  • si la solicitud http está usando el derecho METHOD
  • si la solicitud http tiene el derecho URL
  • si el cuerpo de la respuesta se puede deserializar con éxito con un deserializador json

Si simplemente se burla IStockService en su controlador (por ejemplo), no está probando ninguna de las tres cosas mencionadas anteriormente.

@luisrudge de acuerdo con todos tus puntos. Pero... ¿vale la pena el valor de probar todas estas cosas que enumeras? Quiero decir, si estoy tan comprometido en probar todas esas cosas básicas que están cubiertas por múltiples marcos (json.net, httpclient, etc.), creo que puedo manejar la forma en que es posible hacerlo hoy.

Aún así, si quiero asegurarme de que puedo deserializar alguna respuesta extraña, ¿no puedo simplemente hacer una prueba aislando esos aspectos del código sin la necesidad de agregar HttpClient dentro de la prueba? Además, no hay forma de evitar que el servidor envíe la respuesta correcta que espera en sus pruebas todo el tiempo. ¿Qué pasa si hay una respuesta que no está cubierta? Tus pruebas no lo obtendrán y tendrás problemas de todos modos.

Mi opinión es que las pruebas que propone tienen un valor neto mínimo y debe trabajar en ellas solo si tiene cubierto el otro 99% de su aplicación. Pero al final todo es una cuestión de preferencia personal y este es un tema subjetivo donde diferentes personas ven un valor diferente. Y esto, por supuesto, no significa que HttpClient no deba ser más comprobable.

Creo que el diseño de este código específico tuvo en cuenta todos estos puntos y, aunque no sea el resultado óptimo, no creo que el costo de cambiarlo ahora sea menor que los posibles beneficios.

El objetivo que tenía en mente cuando hablé por primera vez era ayudar a @PureKrome a tratar de no centrar sus esfuerzos en lo que percibo como una tarea de bajo valor. Pero, de nuevo, todo se reduce a la opinión personal en algún momento.

¿Estar seguro de haber accedido a la URL correcta tiene un valor neto bajo para usted? ¿Qué hay de asegurarse de hacer una solicitud PUT para editar un recurso en lugar de una publicación? El otro 99% de mi aplicación puede cubrirse, si esta llamada falla, entonces nada funcionará.

Entonces, realmente creo que hablar de HttpClient::GetStringAsync es una mala manera de demostrar que puedes burlarte de HttpClient en primer lugar. Si está utilizando cualquiera de los métodos HttpClient::Get[String|Stream|ByteArray]Async que ya ha perdido porque no puede manejar correctamente los errores, ya que todo está oculto detrás de un HttpRequestException que se arrojaría y no Ni siquiera expone detalles importantes como el código de estado HTTP. Eso significa que cualquier abstracción que haya creado sobre los HttpClient no tendrá la capacidad de reaccionar y traducir errores HTTP específicos a excepciones específicas del dominio y ahora tiene una abstracción con fugas... y una eso les da a las personas que llaman de su servicio de dominio información de excepción terrible nada menos.

Creo que es importante señalar esto porque, en la mayoría de los casos, esto significa que si estuviera utilizando un enfoque de burla, estaría burlándose de los métodos HttpClient que devuelven HttpResponseMessage que ya lo llevan al la misma cantidad de trabajo que se requiere para simular/falsificar HttpMessageHandler::SendAsync . De hecho, ¡es más! Potencialmente, tendría que simular varios métodos (por ejemplo [Get|Put|Delete|Send]Async ) en el nivel HttpClient en lugar de simplemente simular SendAsync en el nivel HttpMessageHandler .

@luisrudge también mencionó la deserialización de JSON, por lo que ahora estamos hablando de trabajar con HttpContent lo que significa que está trabajando con uno de los métodos que devuelven HttpResponseMessage con seguridad. A menos que esté diciendo que está pasando por alto toda la arquitectura del formateador de medios y deserializando cadenas de GetStringAsync en instancias de objetos manualmente con JsonSerializer::Deserialize o lo que sea. Si ese es el caso, entonces no creo que realmente podamos tener una conversación sobre _probar_ la API más porque eso sería usar la API _en sí misma_ completamente mal. :decepcionado:

@luisrudge , es importante, pero no es necesario tocar HttpClient para asegurarse de que sea correcto. Solo asegúrese de que la fuente de donde lee la URL esté devolviendo las cosas correctamente y que esté listo para comenzar. Nuevamente, no es necesario realizar una prueba de integración con HttpClient .

Estas son cosas importantes, pero una vez que las haces bien, las posibilidades de estropearlo son muy, muy bajas, a menos que tengas una tubería con fugas en otro lugar.

@tucaz de acuerdo. Aún así: es la parte más importante y me gustaría probarla :)

@drub0y Estoy sorprendido de que usar un método público esté mal :)

@luisrudge Bueno, una discusión diferente, supongo, pero no creo que esté defendiendo una interfaz de la que se pueda burlar ignorando las fallas inherentes en el uso de estos métodos de "conveniencia" en todos los escenarios de codificación, excepto en los más reparadores. (por ejemplo, aplicaciones de utilidad y demostraciones de escenario). Si elige usarlos en el código de producción, esa es su elección, pero lo hace mientras reconoce sus deficiencias.

De todos modos, volvamos a lo que @PureKrome quería ver, cuál es la forma en que uno debe diseñar sus clases para que encajen con el diseño de la API HttpClient manera que puedan probar su código fácilmente.

Primero, aquí hay un servicio de dominio de ejemplo que realizará llamadas HTTP para satisfacer las necesidades de su dominio:

```c#
interfaz pública ISomeDomainService
{
TareaGetSomeThingsValueAsync(id de cadena);
}

clase pública SomeDomainService : ISomeDomainService
{
HttpClient privado de solo lectura _httpClient;

public SomeDomainService() : this(new HttpClient())
{

}

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

public async Task<string> GetSomeThingsValueAsync(string id)
{
    return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
}

}

As you can see, it allows an `HttpClient` instance to be injected and also has a default constructor that instantiates the default `HttpClient` with no other special settings (obviously this default instance can be customized, but I'll leave that out for brevity).

Ok, so what's a test method look like for this thing using the MockHttp library then? Let's see:

``` c#
[Fact]
public async Task GettingSomeThingsValueReturnsExpectedValue()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();
    mockHttpMessageHandler.Expect("http://unittest/things/123")
        .Respond(new StringContent("expected value"));

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    var value = await sut.GetSomeThingsValueAsync("123");

    // Assert
    value.Should().Be("expected value");
}

Err, eso es bastante simple y directo; se lee claro como el día IMNSHO. Además, tenga en cuenta que estoy aprovechando FluentAssertions que, nuevamente, es otra biblioteca que creo que muchos de nosotros usamos y otro golpe contra el argumento "No quiero tener que traer otra biblioteca".

Ahora, déjame también ilustrar por qué probablemente nunca _realmente_ querrías usar GetStringAsync para cualquier código de producción. Vuelva a mirar el método SomeDomainService::GetSomeThingsValueAsync y suponga que está llamando a una API REST que en realidad devuelve códigos de estado HTTP significativos. Por ejemplo, tal vez una "cosa" con la identificación de "123" no existe y, por lo tanto, la API devolvería un estado 404. Así que quiero detectar eso y modelarlo como la siguiente excepción específica de dominio:

```c#
// soporte de serialización excluido por brevedad
clase pública SomeThingDoesntExistException: Excepción
{
pública SomeThingDoesntExistException(id de cadena)
{
identificación = identificación;
}

public string Id
{
    get;
    private set;
}

}

Ok, so let's rewrite the `SomeDomainService::GetSomeThingsValueAsync` method to do that now:

``` c#
public async Task<string> GetSomeThingsValueAsync(string id)
{
    try
    {
        return await _httpClient.GetStringAsync("/things/" + Uri.EscapeUriString(id));
    }
    catch(HttpRequestException requestException)
    {
        // uh, oh... no way to tell if it doesn't exist (404) or server maybe just errored (500)
    }
}

De eso estaba hablando cuando dije que esos métodos básicos de "conveniencia" como GetStringAsync no son realmente "buenos" para el código de nivel de producción. Todo lo que puede obtener es HttpRequestException y, a partir de ahí, no puede saber qué código de estado obtuvo y, por lo tanto, no es posible traducirlo a la excepción correcta dentro de un dominio. En su lugar, necesitaría usar GetAsync e interpretar HttpResponseMessage así:

```c#
Tarea asíncrona públicaGetSomeThingsValueAsync(id de cadena)
{
HttpResponseMessage responseMessage = await _httpClient.GetAsync("/things/" + Uri.EscapeUriString(id));

if(responseMessage.IsSuccessStatusCode)
{
    return await responseMessage.Content.ReadAsStringAsync();
}
else
{
    switch(responseMessage.StatusCode)
    {
        case HttpStatusCode.NotFound:
            throw new SomeThingDoesntExistException(id);

        // any other cases you want to might want to handle specifically for your domain

        default:
            // Unhandled cases can throw domain specific lower level communication exceptions
            throw new HttpCommunicationException(responseMessage.StatusCode, responseMessage.ReasonPhrase);
    }
}

}

Ok, so now let's write a test to validate that I get my domain specific exception when I request a URL that doesn't exist:

``` C#
[Fact]
public void GettingSomeThingsValueForIdThatDoesntExistThrowsExpectedException()
{
    // Arrange
    var mockHttpMessageHandler = new MockHttpMessageHandler();

    SomeDomainService sut = new SomeDomainService(new HttpClient(mockHttpMessageHandler)
    {
        BaseAddress = new Uri("http://unittest")
    });

    // Act
    Func<Task> action = async () => await sut.GetSomeThingsValueAsync("SomeIdThatDoesntExist");

    // Assert
    action.ShouldThrow<SomeThingDoesntExistException>();
}

¡¡Ermahgerd es incluso menos código!!$!% ¿Por qué? Porque ni siquiera tuve que configurar una expectativa con MockHttp y automáticamente me devolvió un 404 para la URL solicitada como comportamiento predeterminado.

Magic is Real

@PureKrome también mencionó un caso en el que quiere crear nuevas instancias de HttpClient desde dentro de una clase de servicio. Como dije antes, en ese caso, tomaría Func<HttpClient> como una dependencia en lugar de abstraerse de la creación real o podría introducir su propia interfaz de fábrica si no es fanático de Func por cualquier razón.

Eliminé mi respuesta. Quiero pensar un poco en la respuesta y el código de @drub0y .

@drub0y olvidaste mencionar que alguien tendrá que darse cuenta de que uno necesita implementar un MockHttpMessageHandler en primer lugar. Ese es el punto de esta discusión. No es lo suficientemente sencillo. Con aspnet5, puede probar todo simplemente simulando clases públicas desde interfaces o clases base con métodos virtuales, pero esta entrada de API específica es diferente, menos reconocible y menos intuitiva.

@luisrudge Claro, puedo estar de acuerdo con eso, pero ¿cómo nos dimos cuenta de que necesitábamos un Moq o FakeItEasy? ¿Cómo nos dimos cuenta de que necesitábamos una N/XUnit, FluentAssertions, etc.? Realmente no veo esto como algo diferente.

Hay grandes (inteligentes) personas en la comunidad que reconocen la necesidad de estas cosas y realmente se toman el tiempo para iniciar bibliotecas como estas para resolver estos problemas. (¡Felicitaciones a todos ellos!) Luego, las soluciones que son realmente "buenas" son notadas por otros que reconocen las mismas necesidades y comienzan a crecer orgánicamente a través de OSS y la evangelización de otros líderes en la comunidad. Creo que las cosas están mejor que nunca en este sentido con el poder del OSS finalmente siendo realizado en la comunidad .NET.

Ahora, dicho esto, personalmente creo que Microsoft debería haber proporcionado algo _como_ MockHttp junto con la entrega de las API de System.Net.Http directamente "listas para usar". El equipo de AngularJS proporcionó $httpBackEnd junto con $http porque reconoció que las personas absolutamente _necesitarían_ esta capacidad para poder probar sus aplicaciones de manera efectiva. Creo que el hecho de que Microsoft _no_ identificó este mismo problema es la razón por la que hay tanta "confusión" en torno a las mejores prácticas con esta API y creo que la razón por la que todos estamos aquí teniendo esta discusión al respecto ahora. :decepcionado: dicho esto, me alegro de que @richardszalay y @PureKrome hayan dado un paso al frente para tratar de llenar ese vacío con sus bibliotecas y ahora creo que deberíamos apoyarlos para ayudar a mejorar sus bibliotecas como podamos para hacer todo de nuestras vidas más fácil. :sonreír:

Acabo de recibir una notificación de este hilo, así que pensé en lanzar mi 2c.

Utilizo marcos de simulación dinámicos tanto como cualquier otro, pero en los últimos años he llegado a apreciar que algunos dominios (como HTTP) están mejor servidos si se falsifican detrás de algo con más conocimiento del dominio. Cualquier cosa más allá de la coincidencia básica de URL sería bastante dolorosa con la simulación dinámica y, a medida que itera, terminaría con su propia mini versión de lo que MockHttp hace de todos modos.

Tome Rx como otro ejemplo: IObservable es una interfaz y, por lo tanto, se puede burlar, pero sería una locura probar composiciones complejas (sin mencionar los programadores) solo con burlas dinámicas. La biblioteca de pruebas Rx le da mucho más significado semántico al dominio en el que trabaja.

Volviendo específicamente a HTTP, aunque en el mundo JS, también puede usar sinon para burlarse de XMLHttpRequest, en lugar de usar las API de prueba de Angular, pero me sorprendería mucho si alguien lo hiciera.

Habiendo dicho todo eso, estoy totalmente de acuerdo con la detectabilidad. La documentación de prueba de HTTP de Angular está justo ahí con su documentación de HTTP, pero necesita saber acerca de MockHttp para buscarla. Como tal, estaría totalmente de acuerdo con la contribución de MockHttp a la biblioteca principal, si alguien del equipo de CoreFx quiere ponerse en contacto (las licencias ya son las mismas).

Me gusta burlarme de él, pero también usamos una firma Func :

public static async Task<T> GetWebObjectAsync<T>(Func<string, Task<string>> getHtmlAsync, string url)
{
    var html = await getHtmlAsync(url);
    var info = JsonConvert.DeserializeObject<T>(html);
    return info;
}

Esto permite que el código de la clase Test sea así de simple:

Func<string, Task<string>> getHtmlAsync = u => Task.FromResult(EXPECTED_HTML);
var result = controller.InternalCallTheWeb(getHtmlAsync);

Y el controlador simplemente puede hacer:

public static HttpClient InitHttpClient()
{
    return _httpClient ?? (_httpClient = new HttpClient(new WebRequestHandler
    {
        CachePolicy = new HttpRequestCachePolicy(HttpCacheAgeControl.MaxAge, new TimeSpan(0, 1, 0)),
        Credentials = new NetworkCredential(PublishingCredentialsUserName, PublishingCredentialsPassword)
    }, true));
}

[Route("Information")]
public async Task<IHttpActionResult> GetInformation()
{
    var httpClient = InitHttpClient();
    var result = await InternalCallTheWeb(async u => await httpClient.GetStringAsync(u));
    ...
}

Pero PODRÍAN hacerlo un poco más fácil... :)

¡Hola, equipo de .NET! Solo tocando base en este tema. Ha existido durante más de un mes y solo tengo curiosidad por saber qué piensa el equipo sobre esta discusión.

Ha habido varios puntos de ambos lados del campamento, todos interesantes y relevantes en mi opinión.

¿Hay alguna actualización de ustedes, chicas/chicos?

es decir.

  1. Estamos leyendo la convo y no cambiaremos nada. Gracias por tu aporte, que tengas un hermoso día. Toma, come un pedazo de :pastel:
  2. Honestamente, no hemos pensado en ello (las cosas están muy ocupadas por aquí, como puedes imaginar) _pero_ lo pensaremos en una fecha posterior _antes_ de lanzarlo a la web (RTW).
  3. De hecho, hemos tenido algunas discusiones internas al respecto y reconocemos que al público le resulta un poco doloroso asimilarlo. Así que estamos pensando que podríamos hacer XXXXXXXXX.

¡ejército de reserva!

Es la parte 2 y 3.

Una forma de simplificar la capacidad de insertar un controlador "simulado" para ayudar con las pruebas es agregar un método estático a HttpClient, es decir, HttpClient.DefaultHandler. Este método estático permitirá a los desarrolladores establecer un controlador predeterminado diferente al HttpClientHandler actual al llamar al constructor HttpClient() predeterminado. Además de ayudar con las pruebas en una red "simulada", ayudará a los desarrolladores que escriben código en bibliotecas portátiles por encima de HttpClient, donde no crean directamente instancias de HttpClient. Por lo tanto, les permitiría "inyectar" este controlador predeterminado alternativo.

Por lo tanto, este es un ejemplo de algo que podemos agregar al modelo de objeto actual que sería aditivo y no un cambio radical en la arquitectura actual.

Muchas gracias @davidsh por la pronta respuesta :+1:

Felizmente esperaré y observaré este espacio, ya que es emocionante saber que no es la parte 1 :smile: .. y espero ver cualquier cambio que suceda :+1:

Gracias de nuevo (equipo) por su paciencia y por escuchar, ¡realmente lo aprecio mucho!

/yo felizmente espera.

@davidsh entonces, ¿la interfaz de los métodos virtuales está fuera de discusión?

No está fuera de discusión. Pero la adición de interfaces a corto plazo, como se sugirió en este tema, requiere un estudio y un diseño cuidadosos. No se asigna fácilmente al modelo de objetos existente de HttpClient y, potencialmente, sería un cambio radical en la superficie de la API. Por lo tanto, no es algo que sea rápido o fácil de diseñar/implementar.

¿Qué pasa con los métodos virtuales?

Sí, agregar métodos virtuales no es un cambio radical sino aditivo. Todavía estamos trabajando en qué cambios de diseño son factibles en esta área.

/me arroja Resurrection sobre este hilo...

~ ¡el hilo se retuerce, gime, se contrae y cobra vida!


Acabo de escuchar el Community StandUp del 20 de agosto de 2015 y mencionaron que HttpClient estará disponible de forma cruzada con Beta7. ¡HURRA!

Entonces... ¿ha habido alguna decisión al respecto? No sugiero ni una cosa ni la otra... ¿solo pido cortésmente alguna actualización de la discusión, etc.?

@PureKrome No se ha tomado una decisión al respecto, pero este trabajo de diseño/investigación de la API se incluye en nuestros planes futuros. En este momento, la mayoría de nosotros estamos muy concentrados en llevar el resto del código de System.Net a GitHub y trabajar multiplataforma. Esto incluye tanto el código fuente existente como la migración de nuestras pruebas al marco Xunit. Una vez que este esfuerzo converja, el equipo tendrá más tiempo para comenzar a enfocarse en cambios futuros como los que se sugieren en este hilo.

Muchas gracias @davidsh - Realmente aprecio tu tiempo para responder :) ¡Sigan con el increíble trabajo! :globo:

¿No está un poco obsoleto el uso de simulacros para probar los servicios http? Hay bastantes opciones de alojamiento propio que permiten burlarse fácilmente del servicio http y no requieren cambios de código, solo un cambio en la URL del servicio. Es decir, HttpListener y OWIN self host, pero para una API compleja, incluso se podría crear un servicio simulado o usar una respuesta automática.

image

image

¿Alguna actualización sobre la capacidad de prueba unitaria de esta clase HttpClient? Estoy tratando de simular el método DeleteAsync pero, como se mencionó anteriormente, Moq se queja de que la función no es virtual. Parece que no hay otra manera más fácil que crear nuestro propio contenedor.

Voto por Richardszalay.Mockhttp!
¡Es excelente! Simplemente y brillante!

https://github.com/richardszalay/mockhttp

pero @YehudahA : no deberíamos necesitar eso... (ese es el objetivo de esta conversación) :)

@PureKrome no deberíamos necesitar IHttpClient .
Es la misma idea que EF 7. Nunca usa IDbContext o IDbSet, pero simplemente cambia el comportamiento usando DbContextOptions.

Richardszalay.MockHttp es una solución lista para usar.

Es una solución lista para usar.

no deberíamos necesitar IHttpClient.
Es la misma idea que EF 7. Nunca usa IDbContext o IDbSet, pero simplemente cambia el comportamiento usando DbContextOptions.

Está bien, estoy totalmente en desacuerdo, así que estamos de acuerdo en estar en desacuerdo entonces.

¿Funcionan las dos formas? si. Algunas personas (como yo) encuentran que burlarse de una interfaz o método virtual es MÁS FÁCIL por razones como la facilidad de descubrimiento, la legibilidad y la simplicidad.

A otros (como usted) les gusta manipular el comportamiento. Lo encuentro más complejo, más difícil de descubrir y más lento. (que (para mí) se traduce en más difícil de soportar/mantener).

Cada uno por su cuenta, supongo :)

Richardszalay dice: "Este patrón está fuertemente inspirado en $httpBackend de AngularJS".
Así es. Veamos aquí: https://docs.angularjs.org/api/ngMock/service/ $httpBackend

@PureKrome
Sí, es más simple.
No tiene que saber qué clases y métodos se usan, no necesita una biblioteca simulada/interfaces/métodos virtuales, la configuración no cambiará si cambia de HttpClient a otra cosa.

        private static IDisposable FakeService(string uri, string response)
        {
            var httpListener = new HttpListener();
            httpListener.Prefixes.Add(uri);

            httpListener.Start();

            httpListener.GetContextAsync().ContinueWith(task =>
            {
                var context = task.Result;

                var buffer = Encoding.UTF8.GetBytes(response);

                context.Response.OutputStream.Write(buffer, 0, buffer.Length);

                context.Response.OutputStream.Close();
            });

            return httpListener;
        }

Uso:

            var uri = "http://localhost:8888/myservice/";
            var fakeResponse = "Hello World";

            using (FakeService(uri, fakeResponse))
            {
                // Run your test here ...

                // This is only to test that is really working:
                using (var client = new HttpClient())
                {
                    var result = client.GetStringAsync(uri).Result;
                }
            }

@metzgithub es el mejor aquí, ya que permite una simulación adecuada del comportamiento del servicio de destino.

Es exactamente lo que estaba buscando, ya que me brinda el lugar perfecto para colocar datos 'mal formados/maliciosos' (para pruebas de seguridad y de ruta no feliz)

Llegué tarde a este hilo, llegué aquí después de buscar para ver cómo las personas prueban su dependencia de HttpClient, y encontré algunas formas muy ingeniosas de hacerlo. Declararé desde el principio que en este argumento me inclinaría firmemente por tener una interfaz. Sin embargo, el problema para mí es que no tengo otra opción.

Elijo diseñar y desarrollar componentes y servicios que declaran su dependencia de otros componentes. Prefiero encarecidamente la opción de declarar esas dependencias a través de un constructor y, en tiempo de ejecución, confiar en un contenedor IoC para inyectar una implementación concreta y controlar en este punto si se trata de una instancia única o transitoria.

Elijo usar una definición de capacidad de prueba que aprendí hace aproximadamente una década mientras adoptaba la práctica entonces relativamente nueva de Test Driven Development:

"Una clase es comprobable si se puede eliminar de su entorno normal y aún funciona cuando se inyectan todas sus dependencias"

Otra elección que prefiero hacer es probar la interacción de mis componentes con aquellos de los que dependen. Si declaro una dependencia en un componente, quiero probar que mi componente lo usa correctamente.

En mi humilde opinión, una API bien diseñada me permite trabajar y codificar de la forma que yo elija, algunas limitaciones técnicas son aceptables. Veo que tener solo una implementación concreta de un componente como HttpClient me priva de opciones.

Así que me queda la opción de recurrir a mi solución habitual en estos casos y crear una biblioteca de adaptadores/envolturas liviana que me brinde una interfaz y me permita trabajar de la manera que yo elija.

También llegué un poco tarde a la fiesta, pero incluso después de leer todos los comentarios anteriores, todavía no estoy seguro de cómo proceder. ¿Puede decirme cómo verificaría que los métodos particulares en mi objeto bajo prueba llamen a puntos finales particulares en una API pública? No me importa lo que regrese. Solo quiero asegurarme de que cuando alguien llame a "GoGetSomething" en una instancia de mi clase ApiClient, envíe un GET a " https://somenifty.com/api/something ". Lo que me gustaría hacer es (no funciona el código):

Mock<HttpClient> mockHttpClient = new Mock<HttpClient>();
mockHttpClient.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>())).Verifiable();
ApiClient client = new ApiClient(mockHttpClient.Object);

Something response = await client.GoGetSomething();
mockHttpClient
    .Verify(x => x.SendAsync(
        It.Is<HttpRequestMessage>(m =>
            m.Method.Equals(HttpMethod.Get) &&
            m.RequestUri.Equals("https://somenifty.com/api/something"))));

Pero no me deja burlarme de SendAsync.

SI pudiera hacer funcionar lo básico, entonces podría querer probar cómo mi clase maneja los DEVOLUCIONES particulares de esa llamada también.

¿Algunas ideas? Esto no parece (a mí) como una cosa demasiado fuera de control para querer probar.

Pero no me deja burlarme de SendAsync.

No te burlas del método HttpClient.SendAsync .

En su lugar, crea un controlador simulado derivado de HttpMessageHandler . Luego anula el método SendAsync de eso. Luego, pasa ese objeto controlador al constructor de HttpClient .

@jdcrutchley , puedes hacerlo con MockHttp de @richardszalay.

public class MyApiClient
{
    private readonly HttpClient httpClient;

    public MyApiClient(HttpMessageHandler handler)
    {
        httpClient = new HttpClient(handler, disposeHandler: false);
    }

    public async Task<HttpStatusCode> GoGetSomething()
    {
        var response = await httpClient.GetAsync("https://somenifty.com/api/something");
        return response.StatusCode;
    }
}

[TestMethod]
public async Task MyApiClient_GetSomething_Test()
{
    // Arrange
    var mockHandler = new MockHttpMessageHandler();
    mockHandler.Expect("https://somenifty.com/api/something")
        .Respond(HttpStatusCode.OK);

    var client = new MyApiClient(mockHandler);

    // Act
    var response = await client.GoGetSomething();

    // Assert
    Assert.AreEqual(response, HttpStatusCode.OK);
    mockHandler.VerifyNoOutstandingExpectation();
}

@jdcrutchley o crea su PROPIA clase contenedora y una interfaz contenedora con un método SendAsync .. que simplemente llama al _real_ httpClient.SendAsync (en su clase contenedora).

Supongo que eso es lo que está haciendo MockHttp de @richardszalay.

Ese es el patrón general que hace la gente cuando no puede burlarse de algo (porque no hay interfaz O no es virtual).

mockhttp es un DSL simulado en HttpMessageHandler. No es diferente a burlarse dinámicamente usando Moq, pero la intención es más clara debido al DSL.

La experiencia es la misma de cualquier manera: simule HttpMessageHandler y cree un HttpClient concreto a partir de él. Su componente de dominio depende de HttpClient o HttpMessageHandler.

Editar: TBH, no estoy del todo seguro de cuál es el problema con el diseño existente. La diferencia entre "HttpClient simulado" y "HttpMessageHandler simulado" es literalmente una línea de código en su prueba: new HttpClient(mockHandler) , y tiene la ventaja de que la implementación mantiene el mismo diseño para "Implementación diferente por plataforma" y " Implementación diferente para la prueba" (es decir, no está viciado a los efectos del código de prueba).

La existencia de bibliotecas como MockHttp solo sirve para exponer una API de prueba más limpia y específica del dominio. Hacer que HttpClient sea simulable a través de una interfaz no cambiaría esto.

Como recién llegado a la API de HttpClient, espero que mi experiencia agregue valor a la discusión.

También me sorprendió saber que HttpClient no implementó una interfaz para burlarse fácilmente. Busqué en la web, encontré este hilo y lo leí de principio a fin antes de continuar. La respuesta de @drub0y me convenció de seguir el camino de simplemente burlarme de HttpMessageHandler.

Mi marco de trabajo de simulación de elección es Moq, probablemente bastante estándar para los desarrolladores de .NET. Después de pasar cerca de una hora luchando contra la API de Moq intentando configurar y verificar el método SendAsync protegido, finalmente encontré un error en Moq al verificar los métodos genéricos protegidos. Ver aquí

Decidí compartir esta experiencia para respaldar el argumento de que una interfaz Vanilla IHttpClient me habría hecho la vida mucho más fácil y me habría ahorrado un par de horas. Ahora que llegué a un callejón sin salida, puede agregarme a la lista de desarrolladores que han escrito una combinación simple de contenedor/interfaz alrededor de HttpClient.

Ese es un excelente punto Ken, había olvidado que HttpMessageHandler.SendAsync estaba protegido.

Aún así, mantengo mi reconocimiento de que el diseño de la API se centra primero en la extensibilidad de la plataforma, y ​​no es difícil crear una subclase de HttpMessageHandler (ver mockhttp como ejemplo). Incluso podría ser una subclase que transfiera la llamada a un método público abstracto que sea más compatible con Mock. Me doy cuenta de que parece "impuro", pero podría argumentar la capacidad de prueba frente a la contaminación de la superficie de la API para propósitos de prueba para siempre.

+1 para la interfaz.

Al igual que otros, soy un recién llegado a HttpClient y lo necesito en una clase, comencé a buscar cómo probarlo unitariamente, descubrí este hilo, y ahora, varias horas después, tuve que aprender sobre sus detalles de implementación para probarlo. El hecho de que tantos hayan tenido que transitar este camino es prueba de que este es un verdadero problema para las personas. Y de los pocos que han hablado, ¿cuántos más se limitan a leer el hilo en silencio?

Estoy en medio de liderar un gran proyecto en mi empresa y simplemente no tengo tiempo para perseguir este tipo de rastros de conejos. Una interfaz me habría ahorrado tiempo y dinero a mi empresa, y eso es probablemente lo mismo para muchos otros.

Probablemente seguiré la ruta de crear mi propio envoltorio. Porque mirando desde afuera, no tendrá sentido para nadie por qué inyecté un HttpMessageHandler en mi clase.

Si no es posible crear una interfaz para HttpClient, ¿quizás el equipo de .net pueda crear una solución http completamente nueva que tenga una interfaz?

@scott-martin, HttpClient no hace nada, solo envía las solicitudes a HttpMessageHandler .

En lugar de probar el HttpClient , puede probar y simular el HttpMessageHandler . Es muy simple, y también puedes usar richardszalay mockhttp .

@ scott-martin, como se mencionó solo unos pocos comentarios, no inyecta HttpMessageHandler. Usted inyecta HttpClient e inyecta _eso_ con HttpMessageHandler cuando está realizando pruebas unitarias.

@richardszalay gracias por el consejo, funciona mejor para mantener las abstracciones de bajo nivel fuera de mis implementaciones.

@YehudahA , pude hacerlo funcionar burlándome del HttpMessageHandler. Pero "hacer que funcione" no es la razón por la que intervine. Como dijo @PureKrome , este es un problema de capacidad de descubrimiento. Si hubiera una interfaz, habría podido burlarme de ella y estar en camino en cuestión de minutos. Debido a que no hay ninguno, tuve que pasar varias horas buscando una solución.

Entiendo las limitaciones y vacilaciones para proporcionar una interfaz. Solo quiero dar mi opinión como miembro de la comunidad de que nos gustan las interfaces y, por favor, denos más de ellas: es mucho más fácil interactuar con ellas y probarlas.

este es un problema de capacidad de descubrimiento. Si hubiera una interfaz, habría podido burlarme de ella y estar en camino en cuestión de minutos . Debido a que no hay ninguno, tuve que pasar varias horas buscando una solución.

:arrow_up: esto. Se trata de esto.

EDITAR: énfasis, mío.

Si bien estoy completamente de acuerdo en que podría ser más detectable, no estoy seguro de cómo podría llevar "varias horas". Buscar en Google "simulacro de httpclient" muestra esta pregunta de desbordamiento de pila como el primer resultado, en el que las dos respuestas mejor calificadas (aunque, por supuesto, no es la respuesta aceptada) describen HttpMessageHandler.

NP: todavía estamos de acuerdo en estar en desacuerdo. Esta todo bien :)

puede cerrar este problema, ya que mscorlib regresará con todo su equipaje :)

¿Podemos hacer que estas clases httpclient implementen una interfaz? Entonces podemos ocuparnos del resto por nuestra cuenta.

¿Qué solución propone para burlarse del HttpClient utilizado en un servicio DI?
Estoy tratando de implementar un FakeHttpClientHandler y no sé qué hacer con el método SendAsync.

Tengo un GeoLocationService simple que llama a un microservicio para recuperar información de ubicación basada en ip de otro. Quiero pasar el HttpClientHandler falso al servicio, que tiene dos constructores, uno que recibe la dirección de servicio de ubicación y otro con la dirección de servicio de ubicación y el HttpClientHandler adicional.

public class GeoLocationService: IGeoLocationService {

        private readonly string _geoLocationServiceAddress;
        private HttpClient _httpClient;
        private readonly object _gate = new object();

        /// <summary>
        /// Creates a GeoLocation Service Instance for Dependency Injection
        /// </summary>
        /// <param name="locationServiceAddress">The GeoLocation Service Hostname (IP Address), including port number</param>
        public GeoLocationService(string locationServiceAddress) {
            // Add the ending slash to be able to GetAsync with the ipAddress directly
            if (!locationServiceAddress.EndsWith("/")) {
                locationServiceAddress += "/";
            }

            this._geoLocationServiceAddress = locationServiceAddress;
            this._httpClient = new HttpClient {
                BaseAddress = new Uri(this._geoLocationServiceAddress)
            };
        }

        /// <summary>
        /// Creates a GeoLocation Service Instance for Dependency Injection (additional constructor for Unit Testing the Service)
        /// </summary>
        /// <param name="locationServiceAddress">The GeoLocation Service Hostname (IP Address), including port number.</param>
        /// <param name="clientHandler">The HttpClientHandler for the HttpClient for mocking responses in Unit Tests.</param>
        public GeoLocationService(string locationServiceAddress, HttpClientHandler clientHandler): this(locationServiceAddress) {
            this._httpClient.Dispose();
            this._httpClient = new HttpClient(clientHandler) {
                BaseAddress = new Uri(this._geoLocationServiceAddress)
            };
        }

        /// <summary>
        /// Geo Location Microservice Http Call with recreation of HttpClient in case of failure
        /// </summary>
        /// <param name="ipAddress">The ip address to locate geographically</param>
        /// <returns>A <see cref="string">string</see> representation of the Json Location Object.</returns>
        private async Task<string> CallGeoLocationServiceAsync(string ipAddress) {
            HttpResponseMessage response;
            string result = null;

            try {
                response = await _httpClient.GetAsync(ipAddress);

                response.EnsureSuccessStatusCode();

                result = await response.Content.ReadAsStringAsync();
            }
            catch (Exception ex) {
                lock (_gate) {
                    _httpClient.Dispose(); 
                    _httpClient = new HttpClient {
                        BaseAddress = new Uri(this._geoLocationServiceAddress)
                    };
                }

                result = null;

                Logger.LogExceptionLine("GeoLocationService", "CallGeoLocationServiceAsync", ex.Message, stacktrace: ex.StackTrace);
            }

            return result;
        }

        /// <summary>
        /// Calls the Geo Location Microservice and returns the location description for the provided IP Address. 
        /// </summary>
        /// <param name="ipAddress">The <see cref="string">IP Address</see> to be geographically located by the GeoLocation Microservice.</param>
        /// <returns>A <see cref="string">string</see> representing the json object location description. Does two retries in the case of call failure.</returns>
        public async Task<string> LocateAsync(string ipAddress) {
            int noOfRetries = 2;
            string result = "";

            for (int i = 0; i < noOfRetries; i ++) {
                result = await CallGeoLocationServiceAsync(ipAddress);

                if (result != null) {
                    return result;
                }
            }

            return String.Format(Constants.Location.DefaultGeoLocationResponse, ipAddress);
        }

        public void Dispose() {
            _httpClient.Dispose();
        }

    }

En la clase de prueba, quiero escribir una clase interna llamada FakeClientHandler que extienda HttpClientHandler, que anula el método SendAsync. Otro método sería, supongo, usar un marco simulado para burlarse de HttpClientHandler. Todavía no sé cómo hacer eso. Si pudieras ayudarme, estaría en deuda contigo para siempre.

public class GeoLocationServiceTests {

        private readonly IConfiguration _configuration;
        private IGeoLocationService _geoLocationService;
        private HttpClientHandler _clientHandler;

        internal class FakeClientHandler : HttpClientHandler {

            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {

                // insert implementation here

                // insert fake responses here. 
                return (Task<HttpResponseMessage>) null;
            }

        }

        public GeoLocationServiceTests() {
            this._clientHandler = new FakeClientHandler();
            this._clientHandler.UseDefaultCredentials = true;
            this._geoLocationService = new GeoLocationService("http://fakegeolocation.com/json/", this._clientHandler);
        }


        [Fact]
        public async void RequestGeoLocationFromMicroservice() {
            string ipAddress = "192.36.1.252";

            string apiResponse = await this._geoLocationService.LocateAsync(ipAddress);
            Dictionary<string, string> result = JsonConvert.DeserializeObject<Dictionary<string,string>>(apiResponse);

            Assert.True(result.ContainsKey("city"));

            string timeZone;
            result.TryGetValue("time_zone", out timeZone);

            Assert.Equal(timeZone, @"Europe/Stockholm");
        }

        [Fact]
        public async void RequestBadGeoLocation() {
            string badIpAddress = "asjldf";

            string apiResponse = await this._geoLocationService.LocateAsync(badIpAddress);
            Dictionary<string, string> result = JsonConvert.DeserializeObject<Dictionary<string, string>>(apiResponse);

            Assert.True(result.ContainsKey("city"));

            string city;
            result.TryGetValue("city", out city);
            Assert.Equal(city, "NA");

            string ipAddress;
            result.TryGetValue("ip", out ipAddress);
            Assert.Equal(badIpAddress, ipAddress);
        }
    }

@danielmihai El HttpMessageHandler es efectivamente la "implementación". Por lo general, crea un HttpClient, pasa el controlador al constructor y luego hace que su capa de servicio dependa de HttpClient.

El uso de un marco de simulación genérico es difícil porque el SendAsync reemplazable está protegido. Escribí mockhttp específicamente por esa razón (pero también porque es bueno tener funciones de simulación específicas del dominio.

@danielmihai Lo que dijo Richard resume bastante bien el _dolor_ que todos tenemos que soportar en este momento al burlarnos de HttpClient.

También hay HttpClient.Helpers como _otra_ biblioteca que te ayuda a simular HttpClient... o más específicamente, proporciona un HttpMessageHandler falso para que falsifiques las respuestas.

Lo descubrí, escribí algo como esto... [Creo que he lidiado con el 'dolor']

internal class FakeClientHandler : HttpClientHandler {
            protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
                if (request.Method == HttpMethod.Get) {
                    if (request.RequestUri.PathAndQuery.Contains("/json/")) {
                        string requestPath = request.RequestUri.PathAndQuery;
                        string[] splitPath = requestPath.Split(new char[] { '/' });
                        string ipAddress = splitPath.Last();

                        // RequestGeoLocationFromMicroservice
                        if (ipAddress.Equals(ipAddress1)) {
                            var response = new HttpResponseMessage(HttpStatusCode.OK);
                            response.Content = new StringContent(response1);

                            return Task.FromResult(response);
                        }

                        // RequestBadGeoLocation
                        if (ipAddress.Equals(ipAddress2)) {
                            var response = new HttpResponseMessage(HttpStatusCode.OK);
                            response.Content = new StringContent(response2);
                            return Task.FromResult(response);
                        }
                    }
                }
                return (Task<HttpResponseMessage>) null;
            }
        }

Todas las pruebas pasan

Comente sobre la implementación, si hay mejores formas de hacerlo.

¡Gracias!

Agregando mis pensamientos a esta discusión de más de un año.

@SidharthNabar

Primero, noto que está creando nuevas instancias de HttpClient para enviar cada solicitud; ese no es el patrón de diseño previsto para HttpClient. Crear una instancia de HttpClient y reutilizarla para todas sus solicitudes ayuda a optimizar la agrupación de conexiones y la administración de la memoria. Considere reutilizar una única instancia de HttpClient. Una vez que haga esto, puede insertar el controlador falso en una sola instancia de HttpClient y listo.

¿Dónde me deshago de la instancia no inyectada cuando se supone que no debo usar una instrucción using , sino new en mi instancia de HttpClient, por ejemplo, en el constructor del consumidor? ¿Qué pasa con el HttpMessageHandler que estoy construyendo para inyectar en la instancia de HttpClient?

Dejando a un lado la discusión completa sobre la interfaz o no (estoy totalmente a favor de las interfaces), al menos los documentos podrían indicar que puede inyectar un HttpMessageHandler con fines de prueba. Sí, se menciona el constructor, sí, se menciona que puede inyectar su propio HttpMessageHandler, pero en ninguna parte se indica por qué querría hacer esto.

@richardszalay

Si bien estoy completamente de acuerdo en que podría ser más detectable, no estoy seguro de cómo podría llevar "varias horas". Buscar en Google "simulacro de httpclient" muestra esta pregunta de desbordamiento de pila como el primer resultado, en el que las dos respuestas mejor calificadas (aunque, por supuesto, no es la respuesta aceptada) describen HttpMessageHandler.

Una API debe ser intuitiva de usar con la navegación a través de los documentos, en mi humilde opinión. Tener que hacer una pregunta a Google o SO ciertamente no es intuitivo, la pregunta vinculada que se remonta a mucho antes de .NET Core hace que la actualización no sea obvia (¿tal vez algo cambió entre la pregunta y el lanzamiento de un _nuevo_ marco?)

¡Gracias a todos los que agregaron sus ejemplos de código y proporcionaron bibliotecas auxiliares!

Quiero decir, si tenemos una clase que necesita hacer una llamada http, y queremos hacer una prueba unitaria de esa clase, entonces tenemos que poder burlarnos de ese cliente http. Dado que el cliente http no tiene interfaz, no puedo inyectar un simulacro para ese cliente. En su lugar, tengo que crear un contenedor (que implemente una interfaz) para el cliente http que acepte un controlador de mensajes como un parámetro opcional, y luego enviarle un controlador falso e inyectarlo. Eso es un montón de aros adicionales para saltar por nada. La alternativa es hacer que mi clase tome una instancia de HttpMessageHandler en lugar de HttpClient, supongo...

No me refiero a dividir los pelos, simplemente se siente incómodo y parece un poco poco intuitivo. Creo que el hecho de que este sea un tema tan popular lo respalda.

Dado que el cliente http no tiene interfaz, no podemos inyectarlo con un contenedor.

@dasjestyr Podemos hacer inyección de dependencia sin interfaces. Las interfaces permiten la herencia múltiple; no son un requisito para la inyección de dependencia.

Sí, DI no es el problema aquí. Es lo fácil/bonito que es simularlo, etc.

@shaunluttin Soy consciente de que podemos hacer DI sin una interfaz, no insinué lo contrario; Estoy hablando de una clase que depende de un HttpClient. Sin la interfaz para el cliente, no puedo inyectar un cliente http simulado ni ningún cliente http (para que pueda probarse de forma aislada sin realizar una llamada http real) a menos que lo envuelva en un adaptador que implemente una interfaz que yo hemos definido para imitar el de HttpClient. Cambié un poco la redacción para mayor claridad en caso de que estuviera obsesionado con una sola oración.

Mientras tanto, he estado creando un controlador de mensajes falso en mis pruebas unitarias e inyectando eso, lo que significa que mis clases están diseñadas para inyectar un controlador que supongo que no es lo peor del mundo, pero se siente incómodo. Y las pruebas necesitan falsificaciones en lugar de simulacros/talones. Bruto.

Además, hablando académicamente, las interfaces no permiten la herencia múltiple, solo te permiten imitarla. No hereda interfaces, las implementa; no está _heredando_ ninguna funcionalidad al implementar interfaces, solo declara que ha implementado la funcionalidad de alguna manera. Para imitar la herencia múltiple y alguna funcionalidad ya existente que haya escrito en otro lugar, puede implementar la interfaz y canalizar la implementación a un miembro interno que contiene la funcionalidad real (por ejemplo, un adaptador) que en realidad es composición, no herencia.

Mientras tanto, he estado creando un controlador de mensajes falso en mis pruebas unitarias e inyectando eso, lo que significa que mis clases están diseñadas para inyectar un controlador que supongo que no es lo peor del mundo, pero se siente incómodo. Y las pruebas necesitan falsificaciones en lugar de simulacros/talones. Bruto.

Este es el corazón/núcleo de este problema -> y también es _puramente_ obstinado.

Un lado de la valla: controlador de mensajes falso porque <insert their rational here>
El otro lado de la valla: interfaces (o incluso métodos virtuales, pero eso sigue siendo un poco PITA) debido a <insert their rational here> .

Bueno, quiero decir que las falsificaciones requieren mucho trabajo adicional para configurarse y, en muchos casos, dan como resultado que escribas una clase completamente diferente solo para la prueba que realmente no agrega mucho valor. Los stubs son excelentes porque permitirán que la prueba continúe sin una implementación real. Los simulacros son lo que realmente me interesa porque se pueden usar para afirmaciones. Por ejemplo, afirme que el método en la implementación de esta interfaz se llamó x cantidad de veces con la configuración actual de esta prueba.

De hecho, tuve que probar eso en una de mis clases. Para hacerlo, tuve que agregar un acumulador al controlador falso que se incrementaba cada vez que se llamaba al método SendAsync(). Ese fue un caso simple, afortunadamente, pero vamos. Burlarse es prácticamente un estándar en este punto.

Sí, totalmente de acuerdo contigo aquí 👍 🍰

Por favor, Microsoft, agregue IHttpClient , punto. O sea consistente y elimine todas sus interfaces en el BCL - IList , IDictionary , ICollection , porque son "difíciles de versionar" de todos modos. Diablos, también deshazte de IEnumerable .

Hablando en serio, en lugar de discutir como "puedes escribir tu propio controlador" (claro, pero una interfaz es más fácil y limpia) y "una interfaz introduce cambios importantes" (bueno, eso se hace en todas las versiones de .net de todos modos) , simplemente AGREGAR LA INTERFAZ y dejar que los desarrolladores decidan cómo quieren probarlo.

@lasseschou IList , IDictionary , ICollection y IEnumerable son absolutamente críticos debido a la amplia variedad de clases que los implementan. IHttpClient solo sería implementado por HttpClient - para ser consistente con su lógica, cada clase en el BCL tendría que recibir un tipo de interfaz porque es posible que necesitemos burlarnos de ella.
Me entristecería ver este tipo de desorden.

A menos que haya una compensación dramática, IHttpClient es el nivel de abstracción incorrecto para burlarse de todos modos. Es casi seguro que su aplicación no estará ejerciendo toda la gama de capacidades de HttpClient. Le sugiero que envuelva HttpClient en su propio servicio en términos de las solicitudes específicas de su aplicación y simule su propio servicio.

@ jnm2 : por interés, ¿por qué cree que un IHttpClient es el nivel de abstracción incorrecto para burlarse?

No estoy odiando ni troleando - honesto Q.

(Me gusta aprender, etc.).

Iba a preguntar lo mismo teniendo en cuenta que es un nivel de abstracción bastante perfecto ya que es un cliente.

No es un absoluto. Si está creando un navegador web, entonces podría ver cómo es 1: 1 entre lo que necesita su aplicación y lo que HttpClient resume. Pero si está utilizando HttpClient para hablar con una API de algún tipo, cualquier cosa con expectativas más específicas además de HTTP, solo necesitará una fracción de lo que puede hacer HttpClient. Si crea un servicio que encapsula el mecanismo de transporte de HttpClient y se burla de ese servicio, el resto de su aplicación puede codificar contra la abstracción de la interfaz específica de la API de su servicio, en lugar de codificar contra HttpClient.

Además, me suscribo al principio "Nunca te burles de un tipo que no te pertenece", así que YMMV.
Consulte también http://martinfowler.com/articles/mocksArentStubs.html que compara la prueba clásica con la simulación.

El punto aquí es que HttpClient es un gran avance sobre
WebClient, XMLHttpRequest, que se convirtió en la forma preferida de hacer HTTP
llamadas Es limpio y moderno. Siempre uso adaptadores simulados que hablan
directamente al HttpClient, por lo que probar mi código es fácil. pero me gustaría
pruebe esas clases de adaptadores también. Y ahí es cuando descubro: no hay
Cliente IHttp. Tal vez tengas razón, no es necesario. Tal vez piensas que es
el nivel de abstracción equivocado. Pero sería muy conveniente no
tener que envolverlo en otra clase más en todos los proyectos. simplemente no veo
alguna razón para no tener una interfaz separada para una clase tan importante.

Lasse Schou
Fundador y jefe ejecutivo

http://mouseflow.com
[email protected]

https://www.facebook.com/mouseflowdotcom/
https://twitter.com/mouseflow
https://www.linkedin.com/company/mouseflow

AVISO DE CONFIDENCIALIDAD: Este mensaje de correo electrónico, incluidos los archivos adjuntos, es
para el uso exclusivo de los destinatarios previstos y puede contener datos confidenciales,
información propietaria y/o privilegiada protegida por la ley. Ninguna
Se prohíbe la revisión, el uso, la divulgación o la distribución no autorizados. Si usted
no es el destinatario previsto, póngase en contacto con el remitente por correo electrónico de respuesta
y destruya todas las copias del mensaje original.

El 12 de noviembre de 2016 a las 14:57, Joseph Musser [email protected]
escribió:

No es un absoluto. Si está creando un navegador web, entonces podría ver
cómo es 1: 1 entre lo que necesita su aplicación y lo que HttpClient resume. Pero
si está utilizando HttpClient para hablar con una API de algún tipo, cualquier cosa con
expectativas más específicas además de HTTP, solo necesitará un
fracción de lo que HttpClient puede hacer. Si crea un servicio que
encapsula el mecanismo de transporte HttpClient y simula ese servicio, el
el resto de su aplicación puede codificar contra la interfaz específica de la API de su servicio
abstracción, en lugar de codificar contra HttpClient.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment -260123552, o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/ACnGPp3fSriEJCAjXeAqMZbBcTWcRm9Rks5q9cXlgaJpZM4EPBHy
.

Escucho una entrada extraña de que HttpClient es "demasiado flexible" para ser la capa correcta para simular. Eso parece más un efecto secundario de la decisión de tener una clase con tantas responsabilidades y características expuestas. Pero es realmente independiente de cómo la gente _quiere_ probar el código.

Todavía tengo muchas esperanzas de que solucionemos esto, aunque solo sea porque estamos rompiendo la encapsulación al esperar que los consumidores conozcan estos detalles de cómo se implementa HttpClient (en este caso, para "saber" que es un contenedor que no agrega funcionalidad significativa y envuelve otra clase que pueden o no estar usando). Los consumidores del marco no deberían tener que saber nada sobre la implementación más que exactamente qué interfaz se les expone, y gran parte de la racionalización aquí es que tenemos una solución alternativa y esa profunda familiaridad con la clase ofrece muchas formas no bien encapsuladas de funcionalidad de simulación / stubbing.

¡Arregla por favor!

Me topé con esto, así que arrojé mi 2c...

Creo que burlarse de HttpClient es una locura.

Aunque solo hay un solo SendAsync para simular, hay _tantos_ matices en HttpResponseMessage , HttpResponseHeaders , HttpContentHeaders , etc., sus simulacros van a ensuciarse rápido y probablemente también se estén comportando mal.

¿Me? Uso OwinHttpMessageHandler para

  1. Realice pruebas de aceptación de HTTP desde el exterior de un servicio.
  2. Inyecte servicios HTTP _Dummy_ en componentes que deseen realizar solicitudes HTTP salientes a dichos servicios. Es decir, mi componente tiene un parámetro ctor opcional para HttpMessageHandler .

Estoy seguro de que hay un controlador MVC6 que hace algo similar....

_La vida está bien._

@damianh : ¿Por qué las personas que usan dotnet core deberían hacer algo con Owin para probar una clase como HttpClient? Esto podría tener mucho sentido para un caso de uso particular (AspCore alojado en OWIN), pero esto no parece ser tan general.

Esto realmente parece estar combinando la preocupación de "cuál es la forma más general de prueba unitaria HttpClient que también es consistente con las expectativas del usuario" con "cómo hago la prueba de integración" :).

Oh _testing_ HttpClient... Pensé que el tema era probar cosas que tienen un
dependencia de HttpClient. No te preocupes por mí. :)

El 5 de diciembre de 2016 a las 7:56 p. m., "brphelps" [email protected] escribió:

@damianh https://github.com/damianh : ¿Por qué las personas que usan dotnet core
¿Necesita hacer algo con Owin para probar una clase como HttpClient? Esto podría
tiene mucho sentido para un caso de uso particular (AspCore alojado en OWIN) pero
esto no parece ser tan general.

Esto realmente parece confundir la preocupación de "¿cuál es el problema más general
forma de prueba unitaria HttpClient que también es consistente con las expectativas del usuario"
con "cómo hago la prueba de integración" :).


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-264942511 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AADgXJxunxBgFGLozOsisCoPqjMoch48ks5rFF5vgaJpZM4EPBHy
.

@damianh : son cosas que tienen una dependencia, pero su solución también parece involucrar muchas otras dependencias en la imagen. Me imagino una biblioteca de clases que usa una instancia aprobada en HttpClient (o una DI) -- ¿Dónde entra Owin en la imagen?

No es owin eso es lo interesante que estaba tratando de mostrar (que
también funcionará en .net core / netstandard), así que no se concentre en eso.

Lo interesante es el HttpMessageHandler que hace un proceso
Solicitud HTTP en memoria contra puntos finales HTTP ficticios.

El TestServer de Aspnet Core, al igual que el katana anterior, funciona igual
manera.

Entonces, su clase bajo prueba tiene un HttpClient DI'd donde _it_ tiene un
HttpMessageHandler lo inyectó directamente. Lo comprobable y
la superficie asertiva está ahí.

Burlarse, una forma específica de prueba doble, es algo que todavía recomendaría
en contra de HttpClient. Y por lo tanto, no hay necesidad de un IHttpClient.

El 5 de diciembre de 2016 a las 8:15 p. m., "brphelps" [email protected] escribió:

@damianh https://github.com/damianh -- Son cosas que tienen un
dependencia, pero su solución parece involucrar muchas otras dependencias
en la imagen, también. Estoy imaginando una biblioteca de clases que pasa a utilizar un
pasado en la instancia de HttpClient (o una DI) - ¿Dónde entra Owin?
¿la imagen?


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-264947538 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AADgXEESfQj9NnKLo8LucFLyajWtQXKBks5rFGK8gaJpZM4EPBHy
.

@damianh : comprenda totalmente lo que está diciendo, lo interesante es, pero las personas no necesitan puntos finales HTTP ficticios con una simulación inteligente, por ejemplo, la biblioteca MockHttp de @richardszalay.

Al menos no para pruebas unitarias :).

Depende. Las opciones son buenas :)

El 5 de diciembre de 2016 a las 8:39 p. m., "brphelps" [email protected] escribió:

@damianh https://github.com/damianh -- Comprende totalmente lo que eres
diciendo que lo interesante es, pero la gente no necesita HTTP ficticio
endpoints con un simulacro inteligente, por ejemplo, @richardszalay
Biblioteca MockHttp de https://github.com/richardszalay .

Al menos no para pruebas unitarias :).


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-264954210 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AADgXOl4UEDGYCVLpbvwhueaK52VtjH6ks5rFGhlgaJpZM4EPBHy
.

@damianh dijo:
Las opciones son buenas :)

Sí, puedo estar totalmente de acuerdo con eso 👍 Lo cual, en mi opinión... no tenemos ahora 😢 **

Entiendo totalmente la lógica de crear e inyectar su propio HMH. Eso es lo que estoy haciendo ahora. _Desafortunadamente_, tengo que crear un segundo ctor (o proporcionar un parámetro de ctor opcional) _por cuestiones de pruebas unitarias_. Solo piense en esa oración nuevamente -> estoy creando una OPCIÓN DE USUARIO completamente posible ... que en realidad es solo para habilitar una prueba unitaria para _hacer cosas_. Un usuario final/consumidor de mi clase nunca _realmente_ usará esa opción ctor... que para mí huele mal.

Volviendo al tema de las opciones -> en este momento no siento que tengamos una. Tenemos que _aprender_ sobre el funcionamiento interno de HttpClient . Literalmente abra el capó y mire debajo del capó. (Tuve la suerte de obtener un TL; DR; de Sir Fowler The Awesome ™️ que redujo mi tiempo de investigación).

Entonces, con su punto de no tener interfaz, ¿puede proporcionar algunos ejemplos de cuándo una interfaz sería algo malo, por favor? etc. Recuerde -> Siempre he estado promocionando una interfaz como una _opción_ para ejemplos pequeños/simples a medianos... no el 100% de todos los ejemplos.

No estoy tratando de trolearte o atacarte, Damian, solo estoy tratando de entender.

* Ninja Edit: técnicamente, en realidad tenemos opciones -> no podemos hacer nada / crear nuestros propios envoltorios / etc. Quería decir: no hay muchas opciones que simplifiquen las cosas, * listas para usar .

Hola @PureKrome , no te preocupes, solo comparte la experiencia.

El término 'Prueba de unidad' se ha usado varias veces y pensemos en esto por un minuto. Así que estamos en la misma página, asumo que tenemos un FooClass que tiene una dependencia de ctor en HttpClient (o HMH ) y queremos _simular_ HttpClient .

Lo que realmente estamos diciendo aquí es " FooClass puede hacer una solicitud HTTP usando cualquier URL (protocolo/host/recurso), usando cualquier método, cualquier tipo de contenido, cualquier codificación de transferencia, cualquier codificación de contenido, _cualquiera request headers_, y puede manejar cualquier tipo de contenido de respuesta, cualquier código de estado, cookies, redireccionamientos, codificación de transferencia, encabezados de control de caché, etc. , así como tiempos de espera de red, excepciones de cancelación de tareas, etc.".

HttpClient es como un objeto de Dios ; puedes hacer mucho con eso. HTTP también es una llamada de servicio remoto que va más allá de su proceso.

¿Esto es realmente una prueba unitaria ? Sé honesto ahora :)

** Ninja Edit también: si desea hacer que la unidad FooClass sea comprobable donde desea obtener algo de json de un servidor, tendría una dependencia de ctor en Func<CancellationToken, Task<JObject>> o similar.

¿Puede proporcionar algunos ejemplos de cuándo una interfaz sería algo malo, por favor?

¡No dije eso! Dije que IHttpClient no sería útil porque A) no se sustituirá con una implementación concreta alternativa y B) la preocupación de God Object como se mencionó.

¿Otro ejemplo? IAssembly .

@damianh : el estado de "Clase de Dios" no es una preocupación para las personas que llaman. No es responsabilidad de las personas que llaman asegurarse de que HttpClient sea/no sea una clase de dios. Saben que es el objeto que usan para hacer llamadas HTTP :).

Entonces, suponiendo que aceptamos que el marco .NET nos expuso una clase Dios... ¿cómo podemos reducir nuestra preocupación? ¿Cómo podemos encapsularlo mejor? Eso es fácil: use un marco de simulación tradicional (Moq es un gran ejemplo) y aísle completamente la implementación de HttpClient, porque como personas que lo consumen, simplemente no nos importa cómo está diseñado internamente . Solo nos importa cómo lo usamos. El problema aquí es que el enfoque convencional no funciona y, como resultado, ves personas que encuentran soluciones cada vez más interesantes para resolver el problema :).

Algunas ediciones de gramática y claridad :D

Totalmente de acuerdo , @damianh, en que HttpClient es como una súper Clase de Dios.

Tampoco veo por qué necesito saber acerca de todos los internos nuevamente porque puede haber _tantas permutaciones_ en lo que entra, lo que sale.

Por ejemplo: quiero obtener las acciones de alguna API. https://api.stock-r-us.com/aapl . El resultado es un paylod JSON.

Entonces, las cosas que podría arruinar al intentar llamar a ese punto final:

  • URL de punto final incorrecta?
  • esquema equivocado?
  • ¿faltan encabezados en particular?
  • encabezados incorrectos
  • carga útil de solicitud incorrecta (¿tal vez esto es un POST con algún cuerpo?)
  • error de red/tiempo de espera/etc.
    ...

Así que entiendo que si quiero probar estas cosas, el HMH es una excelente manera de hacerlo porque simplemente secuestra el HMH _real_ y sustituye lo que he dicho, para regresar.

Pero, ¿qué pasa si quiero (bajo mi propio riesgo) ignorar todas estas cosas y solo decir:
_Imagínese que quiero llamar y punto final, todo está bien y obtengo mi buen resultado de carga útil json._
¿Debería seguir aprendiendo y preocuparme por todas estas cosas, para hacer algo tan simple como esto?

O... es esta una mala manera de pensar. ¿Nunca deberíamos pensar así porque es demasiado raro? ¿demasiado barato? demasiado propenso a errores?

La mayoría de las veces solo llamo a un punto final. No pienso en cosas como encabezados, control de caché o errores de red. Por lo general, solo pienso en el VERBO + URL (y CARGA, si es necesario) y luego en el RESULTADO. Tengo un error al manejar esto para Catch-All-The-Things :tm: y luego admito la derrota porque sucedió algo malo :(

Así que o estoy viendo mal el problema (es decir, todavía no puedo deshacerme de mi estado n00b) -o- esta clase de Dios nos está causando un montón de problemas con las pruebas unitarias.

HTTP también es una llamada de servicio remoto que va más allá de su proceso.
¿Esto es realmente una prueba unitaria? Sé honesto ahora :)

Para mí, HttpClient es un _servicio dependiente_. No me importa _qué_ realmente hace el servicio, lo clasifico como _algo que hace algunas cosas que son externas a mi base de código_. Por lo tanto, no quiero que haga cosas con una fuente de alimentación externa, así que quiero controlar eso, secuestrarlo y determinar los resultados de esas llamadas de servicio. Todavía estoy tratando de probar _mi_ unidad de trabajo. Mi pedacito de lógica. Seguro que puede depender de otras cosas. Todavía es una prueba unitaria si puedo controlar todo el universo en el que existe esta pieza de lógica _y_ no tengo que depender/integrarme en otros servicios.

** Otra edición: O tal vez, si HttpClient es en realidad un Objeto de Dios, tal vez la conversación debería tratar de reducirlo a algo ... ¿mejor? ¿O tal vez, no es un Objeto de Dios en absoluto? (solo intento abrir el debate)..

Desafortunadamente, tengo que crear un segundo ctor (o proporcionar un parámetro de ctor opcional) para problemas de pruebas unitarias. Solo piense en esa oración nuevamente -> estoy creando una OPCIÓN DE USUARIO completamente posible ... que en realidad es solo para permitir que una prueba unitaria haga cosas. Un usuario final/consumidor de mi clase nunca usará realmente esa opción de ctor... que para mí huele

No estoy seguro de lo que estás tratando de decir aquí:

"No me gusta tener un segundo ctor que acepte HttpMessageHandler" ... así que haz que tus pruebas lo envuelvan en un HttpClient

"Quiero realizar pruebas unitarias sin invertir mi dependencia de HttpClient"... ¿difícil?

@richardszalay : creo que tengo la sensación de que @PureKrome no quiere cambiar su código de una manera que no mejore el diseño solo para respaldar una estrategia de prueba demasiado obstinada. No me malinterpreten, no me quejo de su biblioteca, la única razón por la que quiero una mejor solución es porque tengo una mayor expectativa de que dotnet core no la requiera =).

No estoy pensando en esto en términos de MockHttp, sino de prueba/burla en general.
Las dependencias invertidas son una necesidad fundamental para las pruebas unitarias en .NET, y eso generalmente implica la inyección de ctor. No tengo claro cuál sería la alternativa propuesta.

Seguramente si HttpClient implementara IHttpClient, todavía habría un constructor que lo aceptara...

Seguramente si HttpClient implementara IHttpClient, todavía habría un constructor que lo aceptara...

Acordado. Estaba "cruzando corrientes" en mi pensamiento cuando literalmente desperté y estaba luchando para que mi célula única en mi cerebro cobrara vida.

Ignoren mi comentario de 'ctor' por favor y continúen con la discusión, si alguien aún no se ha ido aburrido 😄

¿Podemos dar un paso atrás a la pregunta de si el uso HttpClient como una dependencia realmente _puede_ conducir a las pruebas unitarias?

Digamos que tengo una clase FooClass a la que se le inyecta un constructor de dependencias. Ahora quiero realizar una prueba unitaria de los métodos en FooClass , lo que significa que especifico la entrada de mi método, aíslo FooClass lo mejor que puedo de su entorno y verifico el resultado con un valor esperado . En toda esta canalización, no me gustaría preocuparme por las entrañas de mi objeto dependiente; sería mejor para mí si pudiera especificar el tipo de entorno en el que debería vivir mi FooClass : me gustaría simular la dependencia en mi sistema bajo prueba de esa manera que se comporta exactamente como lo necesito para que mis pruebas funcionen.

Ahora, actualmente tengo que preocuparme por la forma en que se implementa HttpClient porque tengo que especificar una dependencia ( HttpMessageHandler ) para mi dependencia inyectada ( HttpClient ) para mi sistema bajo prueba. En lugar de simplemente burlarme de mi dependencia directa, tengo que crear una implementación concreta con una dependencia encadenada simulada.

Tema: no puedo burlarme de HttpClient sin saber cómo funcionan sus componentes internos, y también tengo que confiar en su implementación para no hacer nada sorprendente, ya que no puedo omitir por completo los componentes internos de la clase mediante la burla. Básicamente, esto no sigue a Unit Testing 101: obtener un código de dependencia no relacionado de sus pruebas =).

Puedes ver el tema presente en muchos comentarios diferentes:

"Para mí, HttpClient es un servicio dependiente. No me importa lo que realmente haga el servicio, lo clasifico como algo que hace cosas que son externas a mi base de código". -- PureKrome

"Actualmente, tengo que preocuparme por la forma en que se implementa HttpClient porque tengo que especificar una dependencia (HttpMessageHandler) para mi dependencia inyectada (HttpClient) para mi sistema bajo prueba". -- Thaoden

"Iba a preguntar lo mismo teniendo en cuenta que es un nivel de abstracción bastante perfecto dado que es un cliente". -- dasjestyr

"HttpMessageHandler es efectivamente la 'implementación'. Por lo general, crea un HttpClient, pasa el controlador al constructor y luego hace que su capa de servicio dependa de HttpClient". -- richardszalay

"Pero el problema clave permanecerá: deberá definir un controlador falso e insertarlo debajo del objeto HttpClient". -- SidharthNabar

"HttpClient tiene muchas opciones para la comprobación de unidades. No está sellado y la mayoría de sus miembros son virtuales. Tiene una clase base que se puede usar para simplificar la abstracción, así como un punto de extensión cuidadosamente diseñado en HttpMessageHandler" -- ericstj

"Si la API no se puede burlar, deberíamos arreglarlo. Pero no tiene nada que ver con las interfaces frente a las clases. La interfaz no es diferente de la clase abstracta pura desde la perspectiva de la burla, para todos los propósitos prácticos". -- KrzysztofCwalina

"Con respeto, si la opción es tener que escribir su propio controlador de mensajes falsos, lo cual no es obvio sin profundizar en el código, entonces esa es una barrera muy alta que será muy frustrante para muchos desarrolladores". -- ctolkien

¿Esto es realmente una prueba unitaria? Sé honesto ahora :)

No se. ¿Cómo es la prueba?

Creo que la declaración es un poco como un hombre de paja. El punto es simular la dependencia que es el cliente http, para que el código pueda ejecutarse aislado de esas dependencias... en otras palabras, no hacer una llamada http. Todo lo que estaríamos haciendo es asegurarnos de que los métodos esperados en la interfaz se llamaran en las condiciones adecuadas. Si deberíamos o no haber elegido una dependencia más ligera parece ser una discusión completamente diferente.

HttpClient es la peor dependencia de la historia. Literalmente afirma que el
dependee puede "llamar a Internet". Seguro que todos se burlarán de TCP a continuación. je

El 7 de diciembre de 2016 a las 5:34 a. m., "Jeremy Stafford" [email protected] escribió:

¿Esto es realmente una prueba unitaria? Sé honesto ahora :)

No se. ¿Cómo es la prueba?

Creo que la declaración es un poco como un hombre de paja. El punto es burlarse de la
dependencia que es el cliente http. Todo lo que estaríamos haciendo es asegurarnos de que
los métodos esperados en la interfaz se llamaban debajo de la derecha
condiciones Si deberíamos o no haber elegido una dependencia más ligera parece
ser una discusión completamente diferente.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/corefx/issues/1624#issuecomment-265353526 , o silenciar
la amenaza
https://github.com/notifications/unsubscribe-auth/AADgXPL39vnUbWrUuUBtfmbjhk5IBkxHks5rFjdqgaJpZM4EPBHy
.

Básicamente, esto no sigue a Unit Testing 101: obtener un código de dependencia no relacionado de sus pruebas =).

Personalmente, no creo que esa sea la interpretación correcta de esa práctica en particular. No estamos tratando de probar la dependencia, estamos tratando de probar cómo nuestro código interactúa con esa dependencia , por lo tanto, no está relacionado. Y, simplemente, si no puedo ejecutar una prueba unitaria contra mi código sin invocar la dependencia real, entonces eso no es una prueba aislada. Por ejemplo, si esa prueba de unidad se ejecutó en un servidor de compilación sin acceso a Internet, es probable que la prueba se rompa, de ahí el deseo de burlarse de ella. Podría eliminar la dependencia de HttpClient, pero eso solo significa que terminará en alguna otra clase que usted escribe que hace la llamada, ¿tal vez estoy hablando de esa clase? No lo sabes porque nunca preguntaste. Entonces, ¿mi servicio de dominio debería depender de HttpClient? No claro que no. Sería abstraído a algún otro tipo de servicio abstracto y me burlaría de eso en su lugar. Bien, entonces en la capa que inyecta cualquier abstracción que se me ocurra -- en algún punto de la cadena, _algo_ sabrá sobre HttpClient y ese algo será envuelto por otra cosa que escribo, y esa otra cosa es algo que yo Todavía voy a escribir una prueba unitaria para.

Entonces, si mi prueba de unidad para algún tipo de mediador está probando que una llamada fallida a un servicio en Internet da como resultado que mi código revierta alguna forma de transacción frente a una llamada exitosa a un servicio que resulta en la confirmación de ese transacción, entonces necesito tener control sobre el resultado de esa llamada para establecer las condiciones de esa prueba para poder afirmar que ambos escenarios se están comportando correctamente.

Podría entender la opinión de algunos de que HttpClient no es el nivel correcto de abstracción, sin embargo, no creo que nadie haya preguntado sobre el "nivel" de la clase consumidora, que es un poco presuntivo. Uno podría abstraer la llamada HTTP a un servicio e interfaz, pero algunos de nosotros todavía queremos probar que nuestros "envoltorios" para esa llamada HTTP se comportan como se esperaba, lo que significa que necesitamos controlar la respuesta. Actualmente, esto se puede lograr dándole a HttpClient un controlador falso que es lo que estoy haciendo ahora, y eso está bien, pero el punto es que si quieres esa cobertura, tendrás que pelear con HttpClient en algún momento para al menos fingir la respuesta.

Pero, ahora déjame retroceder. Cuanto más lo miro, es demasiado gordo para una interfaz simulada (lo cual se ha dicho). Creo que puede ser algo mental derivado del nombre "Cliente". No digo que crea que está mal, sino que está un poco fuera de lugar con otras implementaciones de "Cliente" que uno podría ver en la naturaleza en estos días. Vemos muchos "clientes" hoy en día que en realidad son fachadas de servicio, por lo que cuando uno ve el nombre CLIENTE Http, puede pensar lo mismo y ¿por qué no se burlaría de un servicio, verdad?

Ahora veo que el controlador es la parte que importa para el código, por lo que diseñaré en torno a eso en el futuro.

Básicamente, creo que tal vez nuestro deseo de burlarnos de HttpClient está relacionado de alguna manera con la propensión del desarrollador promedio a generar nuevas instancias de HttpClient en lugar de reutilizarlas, lo cual es una práctica terrible. En otras palabras, subestimamos lo que es HttpClient y deberíamos centrarnos en el controlador. Además, el controlador es abstracto, por lo que se puede burlar fácilmente.

Bien, estoy vendido. Ya no quiero la interfaz en HttpClient para burlarme.

Después de luchar durante mucho tiempo, creé mi propia interfaz IHttpHandler e implementé todo para mi caso de uso. Hace que mi código concreto sea más fácil de leer y las pruebas se pueden simular sin algunas cosas raras.

Es casi imposible compartir una instancia de HttpClient en cualquier aplicación realmente tan pronto como necesite enviar un encabezado HTTP diferente en cada solicitud (lo que es crucial cuando se comunica con servicios web RESTful diseñados correctamente). Actualmente HttpRequestHeaders DefaultRequestHeaders está vinculado a la instancia y no a una llamada, lo que lo hace efectivamente con estado. En su lugar {Method}Async() debería aceptarlo, lo que haría que HttpClient no tuviera estado y fuera realmente reutilizable.

@abatishchev Pero puede especificar encabezados en cada HttpRequestMessage.

@richardszalay No digo que sea completamente imposible, digo que HttpClient no fue diseñado bien para este propósito. Ninguno de {Method}Async() acepta HttpRequestMethod , solo SendAsync() lo hace. Pero, ¿cuál es el propósito del resto entonces?

Pero, ¿cuál es el propósito del resto entonces?

Cubrir el 99% de las necesidades.

¿Significa que establecer encabezados es el 1% de los casos de uso? Yo dudo.

De cualquier manera, esto no será un problema si esos métodos tuvieran una sobrecarga al aceptar HttpResponseMessage.

@abatishchev No lo dudo, pero de cualquier manera, escribiría métodos de extensión si me encontrara en su escenario.

Jugué con la idea de tal vez interactuar con HttpMessageHandler y posiblemente HttpRequestMessage porque no me gustaba tener que escribir falsificaciones (vs. simulacros). Pero cuanto más te adentras en ese agujero de conejo, más te das cuenta de que estarás tratando de falsificar objetos de valor de datos reales (por ejemplo, HttpContent), lo cual es un ejercicio inútil. Así que creo que diseñar sus clases dependientes para tomar opcionalmente HttpMessageHandler como un argumento ctor y usar una falsificación para las pruebas unitarias es la ruta más apropiada. Incluso diría que envolver HttpClient también es una pérdida de tiempo...

Esto le permitirá realizar pruebas unitarias de su clase dependiente sin tener que hacer una llamada a Internet, que es lo que desea. Y su falsificación puede devolver códigos de estado y contenido predeterminados para que pueda probar que su código dependiente los está procesando como se esperaba, que es nuevamente lo que realmente desea.

@dasjestyr ¿Intentó crear una interfaz para HttpClient (que es como crear un envoltorio para él) en lugar de interfaces para HttpMessageHandler o HttpRequestMessage ?

/yo curioso.

@PureKrome Esbocé la creación de una interfaz para él y ahí es donde rápidamente me di cuenta de que no tenía sentido. HttpClient realmente solo abstrae un montón de cosas que no importan en el contexto de las pruebas unitarias, y luego llama al controlador de mensajes (que fue un punto que se mencionó varias veces en este hilo). También traté de crear un envoltorio para él, y eso simplemente no valía la pena el trabajo requerido para implementarlo o propagar la práctica (es decir, "yo, todos hagan esto en lugar de usar HttpClient directamente"). Es MUCHO más fácil concentrarse simplemente en el controlador, ya que le brinda todo lo que necesita y se compone literalmente de un solo método.

Dicho esto , creé mi propio RestClient, pero eso resolvió un problema diferente que proporcionaba un generador de solicitudes fluido, pero incluso ese cliente acepta un controlador de mensajes que se puede usar para pruebas unitarias o para implementar controladores personalizados que manejan cosas como cadenas de controladores que resuelven problemas transversales (por ejemplo, registro, autenticación, lógica de reintento, etc.), que es el camino a seguir. Eso no es específico de mi cliente de descanso, es solo un gran caso de uso para configurar el controlador. De hecho, me gusta mucho más la interfaz HttpClient en el espacio de nombres de Windows por este motivo, pero estoy divagando.

Sin embargo, creo que aún podría ser útil para interactuar con el controlador, pero tendría que detenerse allí. Su marco de simulación se puede configurar para devolver instancias predeterminadas de HttpResponseMessage.

Interesante. Descubrí (¿sesgo personal?) que mi biblioteca de ayuda funciona muy bien cuando se usa un controlador de mensajes concreto (falso) ... frente a algunas cosas de interfaz en ese nivel bajo.

Sin embargo, aún preferiría no tener que escribir esa biblioteca o usarla :)

No veo ningún problema con la creación de una pequeña biblioteca para crear falsificaciones. Podría hacerlo cuando esté aburrido y no tenga nada más que hacer. Todas mis cosas de http ya están resumidas y probadas, por lo que no tengo un uso real en este momento. Simplemente no veo ningún valor en envolver HttpClient con el propósito de realizar pruebas unitarias. Fingir al manejador es todo lo que realmente necesitas. Ampliar la funcionalidad es un tema completamente aparte.

Cuando la mayor parte del código base se prueba usando interfaces de simulación, es más conveniente y consistente cuando el resto del código base se prueba de la misma manera. Así que me gustaría ver una interfaz IHttpClient. Como IFileSystemOperarions de ADL SDK o IDataFactoryManagementClient de ADF management SDK, etc.

Sigo pensando que te estás perdiendo el punto, que es que HttpClient no necesita ser burlado, solo el controlador. El verdadero problema es la forma en que la gente ve HttpClient. No es una clase aleatoria que deba actualizarse cada vez que piense llamar a Internet. De hecho, es una buena práctica reutilizar el cliente en toda su aplicación; es un gran problema. Separe las dos cosas y tendrá más sentido.

Además, sus clases dependientes no deberían preocuparse por HttpClient, solo por los datos que devuelve, que provienen del controlador. Piénsalo de esta manera: ¿alguna vez vas a reemplazar la implementación de HttpClient con otra cosa? Posible... pero no probable. Nunca necesita cambiar la forma en que funciona el cliente, entonces, ¿por qué molestarse en abstraerlo? El controlador de mensajes es la variable. Le gustaría cambiar la forma en que se manejan las respuestas, pero no lo que hace el cliente. Incluso la canalización en WebAPI se centra en el controlador (consulte: controlador de delegación). Cuanto más lo digo, más empiezo a pensar que .Net debería hacer que el cliente sea estático y administrarlo por ti... pero quiero decir... lo que sea.

Recuerda para qué son las interfaces. No son para probar, solo fue una forma inteligente de aprovecharlos. Crear interfaces únicamente para ese propósito es ridículo. Microsoft le dio lo que necesita para desacoplar el comportamiento del manejo de mensajes y funciona perfectamente para las pruebas. En realidad, HttpMesageHandler es abstracto, por lo que creo que la mayoría de los marcos burlones como Moq aún funcionarían con él.

Heh @dasjestyr - Yo también creo que podrías haberte perdido un punto importante de mi discusión.

El hecho de que nosotros (los desarrolladores) necesitemos _aprender_ tanto sobre los controladores de mensajes, etc. para falsificar la respuesta es mi _principal_ punto sobre todo esto. No tanto sobre las interfaces. Claro, yo (personalmente) prefiero las interfaces a los envoltorios virtuales r con respecto a las pruebas (y, por lo tanto, a las burlas) ... pero esos son detalles de implementación.

Espero que la esencia principal de este hilo épico sea resaltar que... cuando se usa HttpClient en una aplicación, es un PITA para probarlo.

El statu quo de "ir a aprender la plomería de HttpClient, que lo llevará a HttpMessageHandler, etc." es una situación deficiente. No necesitamos hacer esto para muchas otras bibliotecas, etc.

Así que esperaba que se pudiera hacer algo para aliviar este PITA.

Sí, el PITA es una opinión, lo admito totalmente. Algunas personas no creen que sea un PITA en absoluto, etc.

El verdadero problema es la forma en que la gente ve HttpClient. No es una clase aleatoria que deba actualizarse cada vez que piense llamar a Internet.

¡Acordado! Pero hasta hace poco tiempo, esto no era muy conocido, lo que podría conducir a una falta de documentación o enseñanza o algo así.

Además, sus clases dependientes no deberían preocuparse por HttpClient, solo por los datos que devuelve.

Sí, de acuerdo.

que viene del manipulador.

¿un qué? ¿eh? Oh, ¿ahora me estás pidiendo que abra el capó y aprenda sobre la plomería? ... Consulte lo anterior una y otra vez.

Piénsalo de esta manera: ¿alguna vez vas a reemplazar la implementación de HttpClient con otra cosa?

No.

..etc..

Ahora, no es mi intención trollear, etc. Así que por favor no piensen que estoy tratando de ser un idiota respondiendo y repitiendo siempre las mismas cosas una y otra vez.

He estado usando mi biblioteca de ayuda durante mucho tiempo, que es solo una forma gloriosa de usar un controlador de mensajes personalizado. ¡Genial! Funciona y funciona muy bien. Solo creo que esto podría ser _todo_ expuesto un poco mejor... y eso es lo que espero que este hilo realmente llegue a casa.

EDITAR: formato.

(Acabo de notar el error gramatical en el título de este número y ahora no puedo dejar de verlo)

Y ese ha sido mi _verdadero_ juego largo :)

QED

(en realidad - tan vergonzoso 😊)

EDITO: una parte de mi no quiere editarlo.. por nostalgia....

(Acabo de notar el error gramatical en el título de este número y ahora no puedo dejar de verlo)

¡Sé! ¡Mismo!

El hecho de que nosotros (los desarrolladores) necesitemos aprender tanto sobre los controladores de mensajes, etc. para falsificar la respuesta es mi punto principal sobre todo esto. No tanto sobre las interfaces. Claro, yo (personalmente) prefiero las interfaces a los envoltorios virtuales r con respecto a las pruebas (y, por lo tanto, a las burlas) ... pero esos son detalles de implementación.

¿Qué?

¿Has mirado siquiera a HttpMessageHandler? Es una clase abstracta que es literalmente un único método que toma un HttpRequestMessage y devuelve un HttpResponseMessage, hecho para interceptar el comportamiento separado de toda la basura de transporte de bajo nivel que es exactamente lo que querrías en una prueba unitaria. Para falsificarlo, simplemente impleméntelo. El mensaje que ingresa es el que envió a HttpClient, y la respuesta que sale depende de usted. Por ejemplo, si quiero saber que mi código está tratando con un cuerpo json correctamente, simplemente haga que la respuesta devuelva el cuerpo json que espera. Si quiere ver si está manejando 404 correctamente, entonces pídale que devuelva un 404. No hay nada más básico que eso. Para usar el controlador, simplemente envíelo como un argumento ctor para HttpClient. No necesita sacar ningún cable ni aprender el funcionamiento interno de nada.

Espero que la esencia principal de este hilo épico sea resaltar que... cuando se usa HttpClient en una aplicación, es un PITA para probarlo.

Y la esencia principal de lo que muchas personas han señalado es que lo está haciendo mal (por eso es un PITA, y lo digo de manera directa pero respetuosa) y exigir que HttpClient se conecte con fines de prueba es similar a crear funciones para compensar los errores en lugar de abordar el problema de raíz que, en este caso, es que está operando en el nivel incorrecto.

Creo que HttpClient en el espacio de nombres de Windows en realidad separó el concepto de controlador en filter , pero hace exactamente lo mismo. Creo que el controlador/filtro en esa implementación en realidad está interconectado, que es un poco lo que estaba sugiriendo antes.

¿Qué?
¿Has mirado siquiera a HttpMessageHandler?

Inicialmente no, por esto:

var payloadData = await _httpClient.GetStringAsync("https://......");

es decir, los métodos expuestos en HttpClient son realmente agradables y hacen las cosas _realmente_ fáciles :) ¡Vaya! Rápido, simple, funciona, ¡GANA!

Ok, ahora usémoslo en una prueba... y ahora necesitamos dedicar tiempo a aprender lo que sucede debajo... lo que nos lleva a aprender sobre HttpMessageHandler etc.

Nuevamente, sigo diciendo esto => este aprendizaje adicional sobre la plomería de HttpClient (es decir HMH , etc.) es un PITA. Una vez que haya _hecho ese aprendizaje_... sí, entonces sé cómo usarlo, etc... aunque tampoco me gusta seguir usándolo, pero necesito hacerlo, para simular llamadas remotas a terminales de terceros. Claro, eso es obstinado. Sólo podemos estar de acuerdo en estar en desacuerdo. Entonces, por favor, sé sobre HMH y cómo usarlo.

Y la esencia principal de lo que mucha gente ha señalado es que lo estás haciendo mal.

Sí, la gente no está de acuerdo conmigo. No hay problema. Además, la gente también está de acuerdo conmigo. De nuevo, sin problemas.

y _exigir_ que HttpClient se interconecte con fines de prueba es similar a crear funciones para compensar los errores en lugar de abordar el problema raíz que, en este caso, es que está operando en el nivel incorrecto.

Ok, discrepo respetuosamente contigo aquí (lo cual está bien). De nuevo, diferentes opiniones.

Pero srsly. Este hilo ha durado tanto tiempo. Realmente he avanzado por ahora. Dije mi parte, algunas personas dicen ¡SÍ! algunos dijeron ¡NO! Nada ha cambiado y yo simplemente... acepté el statu quo y acabé con todo.

Lamento que simplemente no aceptes mi línea de pensamiento. No soy una mala persona y trato de no sonar grosero o malo. Solo fui yo expresando cómo me sentí mientras me desarrollaba y cómo creo que otros también se sintieron. Eso es todo.

No estoy en absoluto ofendido. Originalmente salté a este hilo con la misma opinión de que el cliente debería ser burlable, pero luego se hicieron algunos puntos realmente buenos sobre lo que es HttpClient, así que me senté y realmente lo pensé, y luego traté de desafiarlo por mi cuenta y finalmente lo hice. llegó a la misma conclusión, que es que HttpClient es algo incorrecto para burlarse y tratar de hacerlo es un ejercicio inútil que causa mucho más trabajo de lo que vale. Aceptar eso hizo la vida mucho más fácil. Así que yo también he seguido adelante. Solo espero que otros eventualmente se den un respiro y lo tomen por lo que es.

Como un aparte:

Inicialmente no, por esto:

var payloadData = esperar _httpClient.GetStringAsync("https://......");

es decir, los métodos expuestos en HttpClient son realmente agradables y facilitan mucho las cosas :) ¡Yay! Rápido, simple, funciona, ¡GANA!

Yo diría que en términos de SOLID, esta interfaz es inapropiada de todos modos. El cliente de la OMI, la solicitud y la respuesta son 3 responsabilidades diferentes. Puedo apreciar los métodos de conveniencia en el cliente, pero promueve un acoplamiento estrecho al combinar solicitudes y respuestas con el cliente, pero eso es preferencia personal. Tengo algunas extensiones para HttpResponseMessage que logran lo mismo pero mantienen la responsabilidad de leer la respuesta con el mensaje de respuesta. En mi experiencia, con proyectos grandes, "Simple" nunca es "Simple" y casi siempre termina en un BBOM. Sin embargo, esta es una discusión completamente diferente :)

Ahora que tenemos un nuevo repositorio específico para discusiones de diseño, continúe la discusión en https://github.com/dotnet/designs/issues/9 o abra una nueva edición en https://github.com/dotnet/designs

Gracias.

Tal vez podría considerarse la siguiente solución solo con fines de prueba:

clase pública GeoLocationServiceForTest : GeoLocationService, IGeoLocationService
{
GeoLocationServiceForTest público (algo: algo, HttpClient httpClient): base (algo)
{
base._httpClient = httpClient;
}
}

Terminé usando MockHttp de @richardszalay .
¡Gracias, Richard, me salvaste el día!

Ok, puedo vivir con un HttpClient sin interfaz. Pero verse obligado a implementar una clase que hereda HttpMessageHandler porque SendAsync está protegido simplemente apesta.

Yo uso NSubstitute, y esto no funciona:

var httpMessageHandler = 
    Substitute.For<HttpMessageHandler>(); // cannot be mocked, not virtual nor interfaced
httpMessageHandler.SendAsync(message, cancellationToken)
    .Return(whatever); // doesn't compile, it's protected
var httpClient = new HttpClient(httpMessageHandler)

Realmente decepcionante.

¿Fue útil esta página
0 / 5 - 0 calificaciones