简体   繁体   中英

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

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, lets just prentend that the GetAsync and PostAsync methods are virtual ... please :)

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.

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/tim@tigranetworks.co.uk
    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

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

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. Seehttps://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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM