Una sinfonía en C#

Un pequeño aporte a la comunidad de habla hispana.

¿Cómo hacer un test de HttpClient?

Cuando hacemos prueba unitarias (AKA unit testing) uno de los objetivos es aislarnos de los agentes externos para crear un ambiente controlado y que nuestros test se ejecuten bajo condiciones repetibles. Bien, dicho esto uno de las clases que al menos yo me encuentro más seguido es HttpClient, que no hace otra cosa que hacer llamadas HTTP, por desgracia al día de hoy no existe una interfaz para usarla para hacer un mock, entonces tenemos que echar mano de cosas como ésta:

Primera aproximación, hacer un wrapper:

public class HttpClientWrapper
{
    public Uri BaseAddress { get; set; }
    public HttpRequestHeaders DefaultRequestHeaders { get; }
    public TimeSpan Timeout { get; set; }
    public long MaxResponseContentBufferSize { get; set; }

    public void CancelPendingRequests();
    public Task DeleteAsync(string requestUri);
    public Task DeleteAsync(string requestUri, CancellationToken cancellationToken);
....

Algo así pero bien, copiar toooodos los métodos que queremos usar (pero marcados como virtuales) y tener dentro una instancia del verdadero HttpClient a la cual simplemente llamamos en cada uno de nuestro métodos, entonces cuando queremos probar simplemente hacemos un mock donde sobre-escribimos los método que queremos probar.

var mocked = new Mock();
var client = mocked.Object;

mocked.Setup(i => i.SendAsync(It.IsAny())).Returns(Task.FromResult( new HttpResponseMessage()));

Como imaginamos hacer un wrapper suena a mucho trabajo y, seamos realistas, es poco elegante.

Opción dos, heredar HttpMessageHandler

Algo que aprendí hace poco es la existencia de la clase HttpMessageHandler, que es internamente utilizado por HttpClient para hacer las llamadas, y además se puede sobre-escribir.

Entonces, podemos hacer dos cosas, primero crear una versión para probar, simplemente una clase para testing, que nos permita configurar cómo deber responder y después simplemente inyectar la clase en el constructor de HttpClient

public class OffLineResponseHandler : DelegatingHandler
{
    private readonly Dictionary responses = new Dictionary();
    public void AddResponse(Uri uri, HttpResponseMessage responseMessage)
    {
        responses.Add(uri, responseMessage);
    }
    public void AddOkResponse(Uri uri, string content)
    {
        var message = new HttpResponseMessage(HttpStatusCode.OK);
        message.Content = new StringContent(content);
        responses.Add(uri, message);
    }
    public void AddOkResponse(Uri uri)
    {
        this.AddOkResponse(uri, string.Empty);
    }
    public void AddServerErrorResponse(Uri uri)
    {
        var message = new HttpResponseMessage(HttpStatusCode.InternalServerError);
        responses.Add(uri, message);
    }
    public void RemoveAll()
    {
        this.responses.Clear();
    }
    protected async override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        if (responses.ContainsKey(request.RequestUri))
        {
            return responses[request.RequestUri];
        }
        else
        {
            return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request });
        }
    }
}

Esta solución es bastante elegante, permite configurar respuestas con los métodos AddOkResponse y AddServerErrorResponse se agregan a un diccionario que permite grabar respuestas, entonces cuando queremos probar hacemos algo así:

[TestMethod]
public void AddOkResponse()
{
    var target = new HttpClientTest.OffLineResponseHandler();
    var url = new Uri("http://www.fake.com");

    var client = new System.Net.Http.HttpClient(target);

    target.AddOkResponse(url);

    var result = client.GetAsync(url).Result;

    Assert.AreEqual(HttpStatusCode.OK, result.StatusCode);
}

En este ejemplo inyectamos el OfflineResponseHandler y lo inyectamos en el constructor de HttpClient, por otro lado configuramos las respuestas que queremos que de dependiendo de la URL.

La otra opción (es menos trabajo) es una clase que herede de HttpMessageHandler con los métodos Send y SendAsync marcados como virtuales para poder usar un framework de mocking del siguiente modo:

public class FakeHttpMessageHandler : HttpMessageHandler
{
    public virtual HttpResponseMessage Send(HttpRequestMessage request)
    {
        throw new NotImplementedException("Mock this please");
    }

    protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
    {
        return Task.FromResult(Send(request));
    }
}

Entonces cuando queremos probar con simplemente configuramos el framework de mocking así:

var fakeHttpMessageHandler = new Mock { CallBase = true };

fakeHttpMessageHandler.Setup(f => f.Send(It.IsAny())).Returns(new HttpResponseMessage
{
    StatusCode = HttpStatusCode.OK,
    Content = new StringContent("Hi!")
});

Y listo, mismo efecto, simplemente inyectamos en el constructor del HttpClient y tirar millas, podemos probar con bastante facilidad.

Dejo un link con el código de ambas utilidades, nos leemos.

Loading