No me convencen muchas de las respuestas.
En primer lugar, imagine que desea realizar una prueba unitaria de un método que utiliza HttpClient
. No debe crear HttpClient
una instancia directamente en su implementación. Debe inyectar una fábrica con la responsabilidad de proporcionar una instancia de HttpClient
para usted. De esa manera, puede burlarse más tarde de esa fábrica y devolver lo HttpClient
que desee (por ejemplo: un simulacroHttpClient
y no la real).
Entonces, tendrías una fábrica como la siguiente:
public interface IHttpClientFactory
{
HttpClient Create();
}
Y una implementación:
public class HttpClientFactory
: IHttpClientFactory
{
public HttpClient Create()
{
var httpClient = new HttpClient();
return httpClient;
}
}
Por supuesto, necesitaría registrar esta implementación en su contenedor IoC. Si usa Autofac sería algo como:
builder
.RegisterType<IHttpClientFactory>()
.As<HttpClientFactory>()
.SingleInstance();
Ahora tendría una implementación adecuada y comprobable. Imagina que tu método es algo como:
public class MyHttpClient
: IMyHttpClient
{
private readonly IHttpClientFactory _httpClientFactory;
public SalesOrderHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<string> PostAsync(Uri uri, string content)
{
using (var client = _httpClientFactory.Create())
{
var clientAddress = uri.GetLeftPart(UriPartial.Authority);
client.BaseAddress = new Uri(clientAddress);
var content = new StringContent(content, Encoding.UTF8, "application/json");
var uriAbsolutePath = uri.AbsolutePath;
var response = await client.PostAsync(uriAbsolutePath, content);
var responseJson = response.Content.ReadAsStringAsync().Result;
return responseJson;
}
}
}
Ahora la parte de prueba. HttpClient
extiende HttpMessageHandler
, que es abstracto. Creemos un "simulacro" de HttpMessageHandler
que acepte un delegado para que cuando usemos el simulacro también podamos configurar cada comportamiento para cada prueba.
public class MockHttpMessageHandler
: HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
{
_sendAsyncFunc = sendAsyncFunc;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await _sendAsyncFunc.Invoke(request, cancellationToken);
}
}
Y ahora, y con la ayuda de Moq (y FluentAssertions, una biblioteca que hace que las pruebas unitarias sean más legibles), tenemos todo lo necesario para realizar pruebas unitarias en nuestro método PostAsync que usa HttpClient
public static class PostAsyncTests
{
public class Given_A_Uri_And_A_JsonMessage_When_Posting_Async
: Given_WhenAsync_Then_Test
{
private SalesOrderHttpClient _sut;
private Uri _uri;
private string _content;
private string _expectedResult;
private string _result;
protected override void Given()
{
_uri = new Uri("http://test.com/api/resources");
_content = "{\"foo\": \"bar\"}";
_expectedResult = "{\"result\": \"ok\"}";
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var messageHandlerMock =
new MockHttpMessageHandler((request, cancellation) =>
{
var responseMessage =
new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("{\"result\": \"ok\"}")
};
var result = Task.FromResult(responseMessage);
return result;
});
var httpClient = new HttpClient(messageHandlerMock);
httpClientFactoryMock
.Setup(x => x.Create())
.Returns(httpClient);
var httpClientFactory = httpClientFactoryMock.Object;
_sut = new SalesOrderHttpClient(httpClientFactory);
}
protected override async Task WhenAsync()
{
_result = await _sut.PostAsync(_uri, _content);
}
[Fact]
public void Then_It_Should_Return_A_Valid_JsonMessage()
{
_result.Should().BeEquivalentTo(_expectedResult);
}
}
}
Obviamente, esta prueba es una tontería, y realmente estamos probando nuestro simulacro. Pero se entiende la idea. Debe probar la lógica significativa en función de su implementación, como ..
- si el estado del código de la respuesta no es 201, ¿debería lanzar una excepción?
- Si el texto de la respuesta no se puede analizar, ¿qué debería suceder?
- etc.
El propósito de esta respuesta fue probar algo que usa HttpClient y esta es una forma limpia y agradable de hacerlo.
HttpClient
en su interfaz es donde está el problema. Estás obligando a tu cliente a utilizar laHttpClient
clase concreta. En su lugar, debe exponer una abstracción deHttpClient
.