c# - studio - pruebas unitarias mvc 5
Burlándose de HttpClient en pruebas unitarias (17)
Tengo algunos problemas al tratar de ajustar mi código para usarlo en pruebas unitarias. El problema es este. Tengo la interfaz IHttpHandler:
public interface IHttpHandler
{
HttpClient client { get; }
}
Y la clase que lo usa, HttpHandler:
public class HttpHandler : IHttpHandler
{
public HttpClient client
{
get
{
return new HttpClient();
}
}
}
Y luego la clase Connection, que usa simpleIOC para inyectar la implementación del cliente:
public class Connection
{
private IHttpHandler _httpClient;
public Connection(IHttpHandler httpClient)
{
_httpClient = httpClient;
}
}
Y luego tengo un proyecto de prueba de unidad que tiene esta clase:
private IHttpHandler _httpClient;
[TestMethod]
public void TestMockConnection()
{
var client = new Connection(_httpClient);
client.doSomething();
// Here I want to somehow create a mock instance of the http client
// Instead of the real one. How Should I approach this?
}
Ahora, obviamente, tendré métodos en la clase Connection que recuperarán datos (JSON) de mi back-end. Sin embargo, quiero escribir pruebas unitarias para esta clase, y obviamente no quiero escribir pruebas contra el back-end real, sino más bien uno simulado. He intentado buscar en Google una buena respuesta a esto sin gran éxito. Puedo y he usado Moq para burlarme antes, pero nunca en algo como httpClient. ¿Cómo debo abordar este problema?
Gracias por adelantado.
Esta es una vieja pregunta, pero siento la necesidad de extender las respuestas con una solución que no vi aquí.
Puede falsificar el ensamblado de Microsoft (System.Net.Http) y luego usar ShinsContext durante la prueba.
- En VS 2017, haga clic con el botón derecho en el ensamblaje System.Net.Http y seleccione "Agregar ensamblaje de falsificaciones"
- Ponga su código en el método de prueba de la unidad en ShimsContext.Create () usando. De esta manera, puede aislar el código donde planea falsificar el HttpClient.
-
Depende de su implementación y prueba, sugeriría implementar toda la actuación deseada donde llame a un método en el HttpClient y desee falsificar el valor devuelto. El uso de ShimHttpClient.AllInstances falsificará su implementación en todas las instancias creadas durante su prueba. Por ejemplo, si desea falsificar el método GetAsync (), haga lo siguiente:
public class MyHttpClient : IMyHttpClient { private readonly IHttpClientFactory _httpClientFactory; public SalesOrderHttpClient(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task<string> PostAsync(Uri uri, string content) { using (var client = _httpClientFactory.Create()) { var clientAddress = uri.GetLeftPart(UriPartial.Authority); client.BaseAddress = new Uri(clientAddress); var content = new StringContent(content, Encoding.UTF8, "application/json"); var uriAbsolutePath = uri.AbsolutePath; var response = await client.PostAsync(uriAbsolutePath, content); var responseJson = response.Content.ReadAsStringAsync().Result; return responseJson; } } }
Hice algo muy simple, ya que estaba en un entorno DI.
public class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
y el simulacro es:
public static class PostAsyncTests
{
public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
: Given_WhenAsync_Then_Test
{
private SalesOrderHttpClient _sut;
private Uri _uri;
private string _content;
private string _expectedResult;
private string _result;
protected override void Given()
{
_uri = new Uri("http://test.com/api/resources");
_content = "{/"foo/": /"bar/"}";
_expectedResult = "{/"result/": /"ok/"}";
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var messageHandlerMock =
new MockHttpMessageHandler((request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("{/"result/": /"ok/"}")
};
var result = Task.FromResult(responseMessage);
return result;
});
var httpClient = new HttpClient(messageHandlerMock);
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
_sut = new SalesOrderHttpClient(httpClientFactory);
}
protected override async Task WhenAsync()
{
_result = await _sut.PostAsync(_uri, _content);
}
[Fact]
public void Then_It_Should_Return_A_Valid_JsonMessage()
{
_result.Should().BeEquivalentTo(_expectedResult);
}
}
}
Inspirado por la respuesta de PointZeroTwo , aquí hay una muestra usando NUnit y FakeItEasy .
SystemUnderTest
en este ejemplo es la clase que desea probar: no se proporciona contenido de muestra, ¡pero supongo que ya la tiene!
[TestFixture]
public class HttpClientTests
{
private ISystemUnderTest _systemUnderTest;
private HttpMessageHandler _mockMessageHandler;
[SetUp]
public void Setup()
{
_mockMessageHandler = A.Fake<HttpMessageHandler>();
var httpClient = new HttpClient(_mockMessageHandler);
_systemUnderTest = new SystemUnderTest(httpClient);
}
[Test]
public void HttpError()
{
// Arrange
A.CallTo(_mockMessageHandler)
.Where(x => x.Method.Name == "SendAsync")
.WithReturnType<Task<HttpResponseMessage>>()
.Returns(Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.InternalServerError,
Content = new StringContent("abcd")
}));
// Act
var result = _systemUnderTest.DoSomething();
// Assert
// Assert.AreEqual(...);
}
}
Todo lo que necesitas es una versión de prueba de
HttpMessageHandler
clase que pases a
HttpClient
ctor.
El punto principal es que su
HttpMessageHandler
clase de
prueba
tendrá un
HttpRequestHandler
delegado que los llamantes pueden configurar y simplemente manejar de
HttpRequest
la manera que deseen.
public class FakeHttpMessageHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
(r, c) =>
new HttpResponseMessage
{
ReasonPhrase = r.RequestUri.AbsoluteUri,
StatusCode = HttpStatusCode.OK
};
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(HttpRequestHandler(request, cancellationToken));
}
}
Puede usar una instancia de esta clase para crear una instancia concreta de HttpClient. A través del delegado HttpRequestHandler, tiene control total sobre las solicitudes HTTP salientes de HttpClient.
Aquí hay una solución simple, que funcionó bien para mí.
Usando la biblioteca de imitación de moq.
builder
.RegisterType<IHttpClientFactory>()
.As<HttpClientFactory>()
.SingleInstance();
Fuente: https://gingter.org/2018/07/26/how-to-mock-httpclient-in-your-net-c-unit-tests/
Como también se mencionó en los comentarios, debe
abstraer
el
HttpClient
para no estar acoplado a él.
He hecho algo similar en el pasado.
Trataré de adaptar lo que hice con lo que intentas hacer.
Primero mire la clase
HttpClient
y decida qué funcionalidad proporciona que sería necesaria.
Aquí hay una posibilidad:
public interface IHttpClient {
System.Threading.Tasks.Task<T> DeleteAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> DeleteAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(string uri) where T : class;
System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class;
System.Threading.Tasks.Task<T> PostAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PostAsync<T>(Uri uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(string uri, object package);
System.Threading.Tasks.Task<T> PutAsync<T>(Uri uri, object package);
}
Nuevamente, como se dijo antes, esto fue para propósitos particulares.
Extraje por completo la mayoría de las dependencias de cualquier cosa relacionada con
HttpClient
y me concentré en lo que quería que se devolviera.
Debe evaluar cómo desea abstraer el
HttpClient
para proporcionar solo la funcionalidad necesaria que desea.
Esto ahora le permitirá burlarse solo de lo que se necesita para probar.
Incluso recomendaría
IHttpHandler
completamente
IHttpHandler
y usar la abstracción
HttpClient
IHttpClient
.
Pero no estoy eligiendo, ya que puede reemplazar el cuerpo de la interfaz de su controlador con los miembros del cliente abstraído.
Una implementación del
IHttpClient
se puede usar para envolver / adaptar un
HttpClient
real / concreto o cualquier otro objeto para ese asunto, que se puede usar para hacer solicitudes HTTP, ya que lo que realmente quería era un servicio que proporcionara esa funcionalidad como se aplicó a
HttpClient
específicamente.
El uso de la abstracción es un enfoque limpio (en mi opinión) y SÓLIDO y puede hacer que su código sea más fácil de mantener si necesita cambiar el cliente subyacente por otra cosa a medida que cambia el marco.
Aquí hay un fragmento de cómo se podría hacer una implementación.
/// <summary>
/// HTTP Client adaptor wraps a <see cref="System.Net.Http.HttpClient"/>
/// that contains a reference to <see cref="ConfigurableMessageHandler"/>
/// </summary>
public sealed class HttpClientAdaptor : IHttpClient {
HttpClient httpClient;
public HttpClientAdaptor(IHttpClientFactory httpClientFactory) {
httpClient = httpClientFactory.CreateHttpClient(**Custom configurations**);
}
//...other code
/// <summary>
/// Send a GET request to the specified Uri as an asynchronous operation.
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="uri">The Uri the request is sent to</param>
/// <returns></returns>
public async System.Threading.Tasks.Task<T> GetAsync<T>(Uri uri) where T : class {
var result = default(T);
//Try to get content as T
try {
//send request and get the response
var response = await httpClient.GetAsync(uri).ConfigureAwait(false);
//if there is content in response to deserialize
if (response.Content.Headers.ContentLength.GetValueOrDefault() > 0) {
//get the content
string responseBodyAsText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
//desrialize it
result = deserializeJsonToObject<T>(responseBodyAsText);
}
} catch (Exception ex) {
Log.Error(ex);
}
return result;
}
//...other code
}
Como puede ver en el ejemplo anterior, gran parte del trabajo pesado generalmente asociado con el uso de
HttpClient
está oculto detrás de la abstracción.
Su clase de conexión se puede inyectar con el cliente abstraído
public class Connection
{
private IHttpClient _httpClient;
public Connection(IHttpClient httpClient)
{
_httpClient = httpClient;
}
}
Su prueba puede burlarse de lo que se necesita para su SUT
private IHttpClient _httpClient;
[TestMethod]
public void TestMockConnection()
{
SomeModelObject model = new SomeModelObject();
var httpClientMock = new Mock<IHttpClient>();
httpClientMock.Setup(c => c.GetAsync<SomeModelObject>(It.IsAny<string>()))
.Returns(() => Task.FromResult(model));
_httpClient = httpClientMock.Object;
var client = new Connection(_httpClient);
// Assuming doSomething uses the client to make
// a request for a model of type SomeModelObject
client.doSomething();
}
Creo que el problema es que lo tienes un poco al revés.
public class AuroraClient : IAuroraClient
{
private readonly HttpClient _client;
public AuroraClient() : this(new HttpClientHandler())
{
}
public AuroraClient(HttpMessageHandler messageHandler)
{
_client = new HttpClient(messageHandler);
}
}
Si miras la clase anterior, creo que esto es lo que quieres. Microsoft recomienda mantener vivo al cliente para un rendimiento óptimo, por lo que este tipo de estructura le permite hacerlo. Además, el HttpMessageHandler es una clase abstracta y, por lo tanto, imitable. Su método de prueba se vería así:
[TestMethod]
public void TestMethod1()
{
// Arrange
var mockMessageHandler = new Mock<HttpMessageHandler>();
// Set up your mock behavior here
var auroraClient = new AuroraClient(mockMessageHandler.Object);
// Act
// Assert
}
Esto le permite probar su lógica mientras se burla del comportamiento del HttpClient.
Lo siento, muchachos, después de escribir esto y probarlo yo mismo, me di cuenta de que no pueden burlarse de los métodos protegidos en el HttpMessageHandler. Posteriormente agregué el siguiente código para permitir la inyección de un simulacro adecuado.
public interface IMockHttpMessageHandler
{
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
}
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly IMockHttpMessageHandler _realMockHandler;
public MockHttpMessageHandler(IMockHttpMessageHandler realMockHandler)
{
_realMockHandler = realMockHandler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _realMockHandler.SendAsync(request, cancellationToken);
}
}
Las pruebas escritas con esto se parecen a las siguientes:
[TestMethod]
public async Task GetProductsReturnsDeserializedXmlXopData()
{
// Arrange
var mockMessageHandler = new Mock<IMockHttpMessageHandler>();
// Set up Mock behavior here.
var auroraClient = new AuroraClient(new MockHttpMessageHandler(mockMessageHandler.Object));
// Act
// Assert
}
Esta es una pregunta común, y estaba muy del lado de querer la capacidad de burlarse de HttpClient, pero creo que finalmente me di cuenta de que no deberías burlarte de HttpClient. Parece lógico hacerlo, pero creo que nos han lavado el cerebro las cosas que vemos en las bibliotecas de código abierto.
A menudo vemos "Clientes" por ahí que nos burlamos de nuestro código para que podamos probar de forma aislada, por lo que intentamos aplicar automáticamente el mismo principio a HttpClient. HttpClient realmente hace mucho; puedes considerarlo como un administrador de HttpMessageHandler, por lo que no quieres burlarte de eso, y es por eso que todavía no tiene una interfaz. La parte que realmente le interesa para las pruebas unitarias, o el diseño de sus servicios, incluso, es el HttpMessageHandler, ya que eso es lo que devuelve la respuesta, y puede burlarse de eso.
También vale la pena señalar que probablemente debería comenzar a tratar a HttpClient como un negocio más grande. Por ejemplo: Mantenga su instauración de nuevos HttpClients al mínimo. Reutilícelos, están diseñados para ser reutilizados y utilizan una tonelada de menos recursos si lo hace. Si comienza a tratarlo como un problema mayor, se sentirá mucho más mal querer burlarse de él y ahora el controlador de mensajes comenzará a ser lo que está inyectando, no el cliente.
En otras palabras, diseñe sus dependencias alrededor del controlador en lugar del cliente. Aún mejor, los "servicios" abstractos que usan HttpClient que le permiten inyectar un controlador, y lo usan como su dependencia inyectable. Luego, en sus pruebas, puede falsificar el controlador para controlar la respuesta para configurar sus pruebas.
Envolver HttpClient es una pérdida de tiempo increíble.
Actualización: Vea el ejemplo de Joshua Dooms. Es exactamente lo que estoy recomendando.
Estoy de acuerdo con algunas de las otras respuestas en que el mejor enfoque es burlarse de HttpMessageHandler en lugar de envolver HttpClient. Esta respuesta es única, ya que todavía inyecta HttpClient, lo que le permite ser un singleton o administrarse con inyección de dependencia.
"HttpClient está destinado a ser instanciado una vez y reutilizado a lo largo de la vida de una aplicación". ( Source )
Burlarse de HttpMessageHandler puede ser un poco complicado porque SendAsync está protegido. Aquí hay un ejemplo completo, usando xunit y Moq.
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Xunit;
// Use nuget to install xunit and Moq
namespace MockHttpClient {
class Program {
static void Main(string[] args) {
var analyzer = new SiteAnalyzer(Client);
var size = analyzer.GetContentSize("http://microsoft.com").Result;
Console.WriteLine($"Size: {size}");
}
private static readonly HttpClient Client = new HttpClient(); // Singleton
}
public class SiteAnalyzer {
public SiteAnalyzer(HttpClient httpClient) {
_httpClient = httpClient;
}
public async Task<int> GetContentSize(string uri)
{
var response = await _httpClient.GetAsync( uri );
var content = await response.Content.ReadAsStringAsync();
return content.Length;
}
private readonly HttpClient _httpClient;
}
public class SiteAnalyzerTests {
[Fact]
public async void GetContentSizeReturnsCorrectLength() {
// Arrange
const string testContent = "test content";
var mockMessageHandler = new Mock<HttpMessageHandler>();
mockMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage {
StatusCode = HttpStatusCode.OK,
Content = new StringContent(testContent)
});
var underTest = new SiteAnalyzer(new HttpClient(mockMessageHandler.Object));
// Act
var result = await underTest.GetContentSize("http://anyurl");
// Assert
Assert.Equal(testContent.Length, result);
}
}
}
La extensibilidad de HttpClient reside en el
HttpMessageHandler
pasado al constructor.
Su intención es permitir implementaciones específicas de la plataforma, pero también puede burlarse de ella.
No es necesario crear un contenedor de decorador para HttpClient.
Si prefiere un DSL a usar Moq, tengo una biblioteca en GitHub / Nuget que hace las cosas un poco más fáciles: https://github.com/richardszalay/mockhttp
var mockHttp = new MockHttpMessageHandler();
// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localost/api/user/*")
.Respond("application/json", "{''name'' : ''Test McGee''}"); // Respond with JSON
// Inject the handler or client into your application code
var client = new HttpClient(mockHttp);
var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;
var json = await response.Content.ReadAsStringAsync();
// No network connection required
Console.Write(json); // {''name'' : ''Test McGee''}
No estoy convencido por muchas de las respuestas.
En primer lugar, imagine que desea probar un método unitario que utiliza
HttpClient
.
No debe crear una instancia de
HttpClient
directamente en su implementación.
Debe inyectar a una fábrica la responsabilidad de proporcionarle una instancia de
HttpClient
.
De esa manera, puede burlarse más tarde en esa fábrica y devolver el
HttpClient
que desee (por ejemplo: un
HttpClient
simulado y no el real).
Entonces, tendría una fábrica como la siguiente:
// ARRANGE
var handlerMock = new Mock<HttpMessageHandler>(MockBehavior.Strict);
handlerMock
.Protected()
// Setup the PROTECTED method to mock
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
// prepare the expected response of the mocked http call
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("[{''id'':1,''value'':''1''}]"),
})
.Verifiable();
// use real http client with mocked handler here
var httpClient = new HttpClient(handlerMock.Object)
{
BaseAddress = new Uri("http://test.com/"),
};
var subjectUnderTest = new MyTestClass(httpClient);
// ACT
var result = await subjectUnderTest
.GetSomethingRemoteAsync(''api/test/whatever'');
// ASSERT
result.Should().NotBeNull(); // this is fluent assertions here...
result.Id.Should().Be(1);
// also check the ''http'' call was like we expected it
var expectedUri = new Uri("http://test.com/api/test/whatever");
handlerMock.Protected().Verify(
"SendAsync",
Times.Exactly(1), // we expected a single external request
ItExpr.Is<HttpRequestMessage>(req =>
req.Method == HttpMethod.Get // we expected a GET request
&& req.RequestUri == expectedUri // to this uri
),
ItExpr.IsAny<CancellationToken>()
);
Y una implementación:
[TestMethod]
public void FakeHttpClient()
{
using (ShimsContext.Create())
{
System.Net.Http.Fakes.ShimHttpClient.AllInstances.GetAsyncString = (c, requestUri) =>
{
//Return a service unavailable response
var httpResponseMessage = new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
var task = Task.FromResult(httpResponseMessage);
return task;
};
//your implementation will use the fake method(s) automatically
var client = new Connection(_httpClient);
client.doSomething();
}
}
Por supuesto, necesitaría registrar en su IoC Container esta implementación. Si usa Autofac sería algo como:
public class HttpHelper : IHttpHelper
{
private ILogHelper _logHelper;
public HttpHelper(ILogHelper logHelper)
{
_logHelper = logHelper;
}
public virtual async Task<HttpResponseMessage> GetAsync(string uri, Dictionary<string, string> headers = null)
{
HttpResponseMessage response;
using (var client = new HttpClient())
{
if (headers != null)
{
foreach (var h in headers)
{
client.DefaultRequestHeaders.Add(h.Key, h.Value);
}
}
response = await client.GetAsync(uri);
}
return response;
}
public async Task<T> GetAsync<T>(string uri, Dictionary<string, string> headers = null)
{
...
rawResponse = await GetAsync(uri, headers);
...
}
}
Ahora tendría una implementación adecuada y comprobable. Imagine que su método es algo como:
[TestInitialize]
public void Initialize()
{
...
_httpHelper = new Mock<HttpHelper>(_logHelper.Object) { CallBase = true };
...
}
[TestMethod]
public async Task SuccessStatusCode_WithAuthHeader()
{
...
_httpHelper.Setup(m => m.GetAsync(_uri, myHeaders)).Returns(
Task<HttpResponseMessage>.Factory.StartNew(() =>
{
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(_testData))
};
})
);
var result = await _httpHelper.Object.GetAsync<TestDTO>(...);
Assert.AreEqual(...);
}
Ahora la parte de prueba.
HttpClient
extiende
HttpMessageHandler
, que es abstracto.
HttpMessageHandler
un "simulacro" de
HttpMessageHandler
que acepte un delegado para que cuando usemos el simulacro también podamos configurar cada comportamiento para cada prueba.
public interface IHttpClientFactory
{
HttpClient Create();
}
Y ahora, y con la ayuda de Moq (y FluentAssertions, una biblioteca que hace que las pruebas unitarias sean más legibles), tenemos todo lo necesario para probar nuestro método PostAsync que usa
HttpClient
public class HttpClientFactory
: IHttpClientFactory
{
public HttpClient Create()
{
var httpClient = new HttpClient();
return httpClient;
}
}
Obviamente, esta prueba es tonta, y realmente estamos probando nuestro simulacro. Pero se entiende la idea. Debe probar una lógica significativa según su implementación, como ..
- Si el estado del código de la respuesta no es 201, ¿debería arrojar una excepción?
- si el texto de respuesta no se puede analizar, ¿qué debería pasar?
- etc.
El propósito de esta respuesta fue probar algo que usa HttpClient y esta es una buena manera limpia de hacerlo.
Puede usar la biblioteca RichardSzalay MockHttp que se burla del HttpMessageHandler y puede devolver un objeto HttpClient para usar durante las pruebas.
https://github.com/richardszalay/mockhttp
PM> Install-Package RichardSzalay.MockHttp
https://github.com/richardszalay/mockhttp
MockHttp define un HttpMessageHandler de reemplazo, el motor que impulsa HttpClient, que proporciona una API de configuración fluida y proporciona una respuesta fija. La persona que llama (por ejemplo, la capa de servicio de su aplicación) no se da cuenta de su presencia.
https://github.com/richardszalay/mockhttp
var mockHttp = new MockHttpMessageHandler();
// Setup a respond for the user api (including a wildcard in the URL)
mockHttp.When("http://localhost/api/user/*")
.Respond("application/json", "{''name'' : ''Test McGee''}"); // Respond with JSON
// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();
var response = await client.GetAsync("http://localhost/api/user/1234");
// or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result;
var json = await response.Content.ReadAsStringAsync();
// No network connection required
Console.Write(json); // {''name'' : ''Test McGee''}
Sobre la base de las otras respuestas, sugiero este código, que no tiene dependencias externas:
[TestClass]
public class MyTestClass
{
[TestMethod]
public async Task MyTestMethod()
{
var httpClient = new HttpClient(new MockHttpMessageHandler());
var content = await httpClient.GetStringAsync("http://some.fake.url");
Assert.AreEqual("Content as string", content);
}
}
public class MockHttpMessageHandler : HttpMessageHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Content as string")
};
return await Task.FromResult(responseMessage);
}
}
Su interfaz expone la clase concreta
HttpClient
, por lo tanto, cualquier clase que use esta interfaz está vinculada a ella, esto significa que no se puede burlar.
HttpClient
no hereda de ninguna interfaz, por lo que tendrá que escribir la suya.
Sugiero un patrón
tipo decorador
:
public interface IHttpHandler
{
HttpResponseMessage Get(string url);
HttpResponseMessage Post(string url, HttpContent content);
Task<HttpResponseMessage> GetAsync(string url);
Task<HttpResponseMessage> PostAsync(string url, HttpContent content);
}
Y tu clase se verá así:
public class HttpClientHandler : IHttpHandler
{
private HttpClient _client = new HttpClient();
public HttpResponseMessage Get(string url)
{
return GetAsync(url).Result;
}
public HttpResponseMessage Post(string url, HttpContent content)
{
return PostAsync(url, content).Result;
}
public async Task<HttpResponseMessage> GetAsync(string url)
{
return await _client.GetAsync(url);
}
public async Task<HttpResponseMessage> PostAsync(string url, HttpContent content)
{
return await _client.PostAsync(url, content);
}
}
El punto de todo esto es que
HttpClientHandler
crea su propio
HttpClient
, por supuesto, podría crear múltiples clases que implementen
IHttpHandler
de diferentes maneras.
El problema principal con este enfoque es que está escribiendo efectivamente una clase que solo llama a métodos en otra clase, sin embargo, podría crear una clase que
herede
de
HttpClient
(vea
el ejemplo de Nkosi
, es un enfoque mucho mejor que el mío).
La vida sería mucho más fácil si
HttpClient
tuviera una interfaz que pudiera burlarse, desafortunadamente no la tiene.
Sin embargo, este ejemplo
no
es el boleto dorado.
IHttpHandler
todavía se basa en
HttpResponseMessage
, que pertenece al espacio de nombres
System.Net.Http
, por lo tanto, si necesita otras implementaciones que no sean
HttpClient
, tendrá que realizar algún tipo de mapeo para convertir sus respuestas en objetos
HttpResponseMessage
.
Por supuesto, esto solo es un problema
si necesita usar múltiples implementaciones
de
IHttpHandler
pero no parece que lo haga, así que no es el fin del mundo, pero es algo en lo que debe pensar.
De todos modos, puede simplemente burlarse de
IHttpHandler
sin tener que preocuparse por la clase concreta
HttpClient
, ya que se ha abstraído.
Recomiendo probar los métodos no asíncronos , ya que estos aún se denominan métodos asincrónicos, pero sin la molestia de tener que preocuparse por la prueba unitaria de métodos asincrónicos, consulte here
Una alternativa sería configurar un servidor HTTP de código auxiliar que devuelva respuestas enlatadas en función del patrón que coincida con la url de la solicitud, lo que significa que prueba las solicitudes HTTP reales, no simulacros. Históricamente, esto habría requerido un esfuerzo de desarrollo significativo y habría sido muy lento para ser considerado para las pruebas unitarias, sin embargo, la biblioteca OSS WireMock.net es fácil de usar y lo suficientemente rápida como para ejecutarse con muchas pruebas, por lo que vale la pena considerarla. La configuración es unas pocas líneas de código:
var server = FluentMockServer.Start();
server.Given(
Request.Create()
.WithPath("/some/thing").UsingGet()
)
.RespondWith(
Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBody("{''attr'':''value''}")
);
Puede encontrar más detalles y orientación sobre el uso de wiremock en las pruebas aquí.
Unirme a la fiesta un poco tarde, pero me gusta usar wiremocking ( WireMock.net ) siempre que sea posible en la prueba de integración de un microservicio central dotnet con dependencias REST posteriores.
Al implementar un TestHttpClientFactory extendiendo el IHttpClientFactory podemos anular el método
HttpClient CreateClient (nombre de cadena)
Entonces, al usar los clientes nombrados dentro de su aplicación, usted tiene el control de devolver un HttpClient conectado a su wiremock.
Lo bueno de este enfoque es que no está cambiando nada dentro de la aplicación que está probando, y permite que las pruebas de integración del curso realicen una solicitud REST real a su servicio y se burlen del json (o lo que sea) que la solicitud aguas abajo real debería devolver. Esto lleva a pruebas concisas y la menor burla posible en su aplicación.
public class TestHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
var httpClient = new HttpClient
{
BaseAddress = new Uri(G.Config.Get<string>($"App:Endpoints:{name}"))
// G.Config is our singleton config access, so the endpoint
// to the running wiremock is used in the test
};
return httpClient;
}
}
y
// in bootstrap of your Microservice
IHttpClientFactory factory = new TestHttpClientFactory();
container.Register<IHttpClientFactory>(factory);
Uno de mis colegas notó que la mayoría de los métodos
HttpClient
llaman a
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
bajo el capó, que es un método virtual fuera de
HttpMessageInvoker
:
Entonces, con mucho, la forma más fácil de burlarse de
HttpClient
era simplemente burlarse de ese método en particular:
var mockClient = new Mock<HttpClient>();
mockClient.Setup(client => client.SendAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>())).ReturnsAsync(_mockResponse.Object);
y su código puede llamar a la mayoría (pero no a todos) de los métodos de la clase
HttpClient
, incluido un método regular
httpClient.SendAsync(req)
Marque aquí para confirmar https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs