How can I use FakeItEasy with HttpClient, in a unit test?
Asked Answered
E

5

11

I'm trying to figure out how to use FakeItEasy with the HttpClient, given the following code:

public Foo(string key, HttpClient httpClient = null)
{ .. }

public void DoGet()
{
    ....

    if (_httpClient == null)
    {
        _httpClient = new HttpClient();
    }

    var response = _httpClient.GetAsync("user/1");
}

public void DoPost(foo Foo)
{
    if (_httpClient == null)
    {
        _httpClient = new HttpClient();
    }

    var formData = new Dictionary<string, string>
    {
        {"Name", "Joe smith"},
        {"Age", "40"}
    };    

    var response = _httpClient.PostAsync("user", 
        new FormUrlEncodedContent(formData));
}

So I'm not sure how to use FakeItEasy, to fake out the HttpClient's GetAsync and PostAsync methods.

Production code will not pass in the HttpClient, but the unit test will pass in the fake instance, made by FakeItEasy.

eg.

[Fact]
public void GivenBlah_DoGet_DoesSomething()
{
    // Arrange.
    var httpClient A.Fake<HttpClient>(); // <-- need help here.
    var foo = new Foo("aa", httpClient);

    // Act.
    foo.DoGet();

    // Assert....
}

Update

I grok that FiE (and most mocking packages) works on interfaces or virtual methods. So for this question, let's just prentend that the GetAsync and PostAsync methods are virtual.

Englebert answered 7/3, 2014 at 0:42 Comment(1)
I stand by my usual answer to this: don't unit test your bottom level I/O layer - the ROI is too lowWilburnwilburt
B
22

Here's my (more or less) general purpose FakeHttpMessageHandler.

public class FakeHttpMessageHandler : HttpMessageHandler
{
    private HttpResponseMessage _response;

    public static HttpMessageHandler GetHttpMessageHandler( string content, HttpStatusCode httpStatusCode )
    {
        var memStream = new MemoryStream();

        var sw = new StreamWriter( memStream );
        sw.Write( content );
        sw.Flush();
        memStream.Position = 0;

        var httpContent = new StreamContent( memStream );

        var response = new HttpResponseMessage()
        {
            StatusCode = httpStatusCode,
            Content = httpContent
        };

        var messageHandler = new FakeHttpMessageHandler( response );

        return messageHandler;
    }

    public FakeHttpMessageHandler( HttpResponseMessage response )
    {
        _response = response;
    }

    protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
    {
        var tcs = new TaskCompletionSource<HttpResponseMessage>();

        tcs.SetResult( _response );

        return tcs.Task;
    }
}

Here is an example of it being used from one of my tests that expects some JSON as a return value.

const string json = "{\"success\": true}";

var messageHandler = FakeHttpMessageHandler.GetHttpMessageHandler( 
    json, HttpStatusCode.BadRequest );
var httpClient = new HttpClient( messageHandler );

You would now inject httpClient into your class under test (using whatever injection mechanism you prefer) and when GetAsync is called your messageHandler will spit back the result you told it to.

Bendwise answered 7/3, 2014 at 16:17 Comment(3)
That's really really really helpful, Craig! This brings me to another question - why are you creating a custom message handler, instead of just an HttpClient wrapper?Englebert
I find this to be cleaner than using a wrapper. In my production code I can continue to use HttpClient instead of having to remember to use the wrapper. From experience I find wrappers to just be a PITA. Maybe for something like HttpClient, where you only have a couple of methods, it's not so bad. I think it's largely a matter of preference and what your established standards are which approach you take.Bendwise
Thank you Craig. You helped me tremendously.Englebert
I
7

You could also create an AbstractHandler on which you can intercept a public abstract method. For instance:

public abstract class AbstractHandler : HttpClientHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(SendAsync(request.Method, request.RequestUri.AbsoluteUri));
    }

    public abstract HttpResponseMessage SendAsync(HttpMethod method, string url);
}

Then you can intercept calls to the AbstractHandler.SendAsync(HttpMethod method, string url) like:

// Arrange
var httpMessageHandler = A.Fake<AbstractHandler>(options => options.CallsBaseMethods());
A.CallTo(() => httpMessageHandler.SendAsync(A<HttpMethod>._, A<string>._)).Returns(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Result")});
var httpClient = new HttpClient(httpMessageHandler);

// Act
var result = await httpClient.GetAsync("https://google.com/");

// Assert
Assert.Equal("Result", await result.Content.ReadAsStringAsync());
A.CallTo(() => httpMessageHandler.SendAsync(A<HttpMethod>._, "https://google.com/")).MustHaveHappenedOnceExactly();

More information can be found on this blog: https://www.meziantou.net/mocking-an-httpclient-using-an-httpclienthandler.htm

Infusive answered 16/9, 2019 at 6:23 Comment(2)
I believe this will fail if I have return type other than "HttpResponseMessage" .. ?Kazukokb
This is much cleaner than Craig's answer (even though his approach is as good). Thanks, this helped me a lot!Lingonberry
H
3

I did something like this when I needed to interact with the Gravatar service. I tried to use fakes/mocks but found it was impossible with HttpClient. Instead, I came up with a custom HttpMessageHandler class that lets me pre-load the expected response, along these lines:

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Tigra.Gravatar.LogFetcher.Specifications
    {
    /// <summary>
    ///   Class LoggingHttpMessageHandler.
    ///   Provides a fake HttpMessageHandler that can be injected into HttpClient.
    ///   The class requires a ready-made response message to be passed in the constructor,
    ///   which is simply returned when requested. Additionally, the web request is logged in the
    ///   RequestMessage property for later examination.
    /// </summary>
    public class LoggingHttpMessageHandler : DelegatingHandler
        {
        internal HttpResponseMessage ResponseMessage { get; private set; }
        internal HttpRequestMessage RequestMessage { get; private set; }

        public LoggingHttpMessageHandler(HttpResponseMessage responseMessage)
            {
            ResponseMessage = responseMessage;
            }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
            {
            RequestMessage = request;
            return Task.FromResult(ResponseMessage);
            }
        }
    }

Then my test context setup goes something like this:

public class with_fake_gravatar_web_service
    {
    Establish context = () =>
        {
        MessageHandler = new LoggingHttpMessageHandler(new HttpResponseMessage(HttpStatusCode.OK));
        GravatarClient = new HttpClient(MessageHandler);
        Filesystem = A.Fake<FakeFileSystemWrapper>();
        Fetcher = new GravatarFetcher(Committers, GravatarClient, Filesystem);
        };

    protected static LoggingHttpMessageHandler MessageHandler;
    protected static HttpClient GravatarClient;
    protected static FakeFileSystemWrapper Filesystem;
    }

Then, here's an example of a test (specification) that uses it:

[Subject(typeof(GravatarFetcher), "Web service")]
public class when_fetching_imagaes_from_gravatar_web_service : with_fake_gravatar_web_service
    {
    Because of = () =>
        {
        var result = Fetcher.FetchGravatars(@"c:\"); // This makes the web request
        Task.WaitAll(result.ToArray());
        //"http://www.gravatar.com/avatar/".md5_hex(lc $email)."?d=404&size=".$size; 
        UriPath = MessageHandler.RequestMessage.RequestUri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
        };

    It should_make_request_from_gravatar_dot_com =
        () => MessageHandler.RequestMessage.RequestUri.Host.ShouldEqual("www.gravatar.com");

    It should_make_a_get_request = () => MessageHandler.RequestMessage.Method.ShouldEqual(HttpMethod.Get);
    // see https://en.gravatar.com/site/check/[email protected]
    It should_request_the_gravatar_hash_for_tim_long =
        () => UriPath.ShouldStartWith("avatar/df0478426c0e47cc5e557d5391e5255d");

    static string UriPath;
    }

You can see the full source at http://stash.teamserver.tigranetworks.co.uk/users/timlong/repos/tigra.gravatar.logfetcher/browse

Havelock answered 7/3, 2014 at 1:26 Comment(1)
on a side note: what testing framework is that? It reminds me of NSpec, which I'm using.Pestana
I
2

FakeItEasy, like most mocking libraries, does not create proxies for non-abstract components. In the case of HttpClient, the GetAsync and PostAsync methods are neither virtual nor abstract, so you can't create stub implementations of them directly. See https://github.com/FakeItEasy/FakeItEasy/wiki/What-can-be-faked.

In this case, you need a different abstraction as a dependency - one which HttpClient can fulfill, but so could other implementations, including mocks/stubs.

It answered 7/3, 2014 at 1:3 Comment(2)
Essentially, yeah. I'd define an interface and have a concrete implementation of it that acts as a proxy to HttpClient used at run-time, with a mock used for tests.It
You can create a wrapper but that can get to be a bit of a PITA to maintain. Creating a HttpMessageHandler that can be faked and injected is a better solution. I've got a general purpose class that will do that at work, I'll post it tomorrow.Bendwise
T
-1

This isn't answering your question directly, but I wrote a library a while back that provides an API for stubbing out requests/responses. It's pretty flexible, and supports ordered/unordered matching as well as a customisable fallback system for unmatched requests.

It's available on GitHub here: https://github.com/richardszalay/mockhttp

Toccaratoccata answered 18/11, 2015 at 22:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.