Flurl icon indicating copy to clipboard operation
Flurl copied to clipboard

Using HttpTest's AllowRealHttp doesn't appear to actually return integration test localhost calls to true default http request behavior

Open ryan-singleton opened this issue 2 years ago • 6 comments

I'm trying to write integration tests for a .NET 6 AspNetCore service.

By default, as it is an integration test, I want the Flurl http calls to do their actual http logic. However, there is one api that we are hitting where files are uploaded within the processors. That service is already tested elsewhere, so it would not be ideal to post many files during our testing strategy for no real value.

So I want to mock the behavior of calls to that API and only that API.

It would seem, then, that the answer would be to treat it something like this:

using var httpTest = new HttpTest();
     httpTest.ForCallsTo("*localhost*").AllowRealHttp();
     httpTest.ForCallsTo("*dataocean/api*")
        .RespondWithJson(new ClientJsonReply<FileResponse>(fileResponse, "success", new HttpResponseMessage()))
        .RespondWith("success");

However, this results in the following exception "Call failed. No connection could be made because the target machine actively refused it. (localhost:80): PUT http://localhost/terms"

However, if I remove the HttpTest from this test, everything works. We just end up barraging the file storage API, which I do not want to do. We're getting the local base address from our WebApplicationFactory default.

What this tells me is that calling "httpTest.AllowRealHttp()" does not return the internally stored HttpClient to its default behavior for the url that I specify, such as locahost.

I apologize if I missed some documentation somewhere on this matter. Any help would be appreciated.

ryan-singleton avatar Mar 23 '22 22:03 ryan-singleton

Have you tried reversing the order of those setup calls? Think of ForCallsTo as adding match conditions to a list (because it literally does that), and when a call is made in the SUT it stops at the first match it finds in that list. So in your case I think AlllowRealHttp should be your catch-all, i.e. specify it last.

tmenier avatar Mar 24 '22 01:03 tmenier

Apologies, that was actually the first way that I tried it. The result was the same, so I dropped my assumption that this was how it works when I had the same result both ways.

httpTest.ForCallsTo("*dataocean/api*")
      .RespondWithJson(new ClientJsonReply<FileResponse>(fileResponse, "success", new HttpResponseMessage()))
      .RespondWith("success");
httpTest.ForCallsTo($"*localhost*").AllowRealHttp();

This is what I set it back to, but I'm still getting:

System.Net.Http.HttpRequestException No connection could be made because the target machine actively refused it. (localhost:80) at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken)

Edit: Let me be a little more informative on how I got here.

So I have a test class called ProgramFixture which inherits from WebApplicationFactory<Program> and it performs all of my integration test extended setup. Pretty standard at this point, so I don't expect that code is needed from this. So this class has a method called CreateClient() which returns an HttpClient that just verifiably works fine on its own in these tests.

However, my web calls go through Flurl, so my test class creates a FlurlClient from that method like this var flurlClient = new FlurlClient(_programFixture.CreateClient());

Again, everything still works fine like this.

But as soon as I create this HttpTest class, SUT's within its lifetime calling to localhost fail due to the connection refused exception.

Hopefully that helps.

ryan-singleton avatar Mar 24 '22 13:03 ryan-singleton

Sorry for the delay and hopefully you found some kind of work-around for this by now, but the extra info you provided is helpful. Basically, AllowRealHttp does not cause Flurl to switch back to the HttpClient instance you provided in the FlurlClient constructor. It will continue to use the "testing" HttpClient instance, partly so that the test framework can still log those calls so you can still assert against them. The only thing AllowRealHttp does is disable the behavior that blocks calls from reaching the inner-most message handler (which actually sends the request).

I can see how this is a problem with ASP.NET Core's integration testing features, especially since I've recommended doing exactly what you're doing. I don't know all the details about that client returned by WebApplicationFactory, but I imagine it does some sort of HTTP "faking" of its own and I probably need to prevent Flurl from getting in the way of that when there's an HttpTest at play. I'll think on this. I'm planning a possible ASP.NET Core companion lib for Flurl anyway so this is good timing.

tmenier avatar May 04 '22 22:05 tmenier

Awesome. For now we just used Moq and made a mock for requests to that API alone.

As you probably know, that stuff gets pretty verbose, so I'll be excited to swap out for your test pattern once it becomes compatible with this. Thank you for the answer, Todd.

ryan-singleton avatar May 11 '22 14:05 ryan-singleton

I'll actually keep this open for my own tracking purposes. There will tentatively be "a" fix for this of some sort, at some point. :)

tmenier avatar May 11 '22 17:05 tmenier

Oops, apologies about that. Makes sense though.

ryan-singleton avatar May 11 '22 18:05 ryan-singleton

This fix triggered a refactoring binge that actually yielded some nice simplifications. Several classes were removed that I doubt anyone would typically use directly, but since they're marked public this is technically a breaking change. The removed classes:

  • FakeHttpMessageHandler
  • TestHttpClientFactory
  • TestFlurlHttpSettings (HttpTest.Settings is now an instance of FlurlHttpSettings)

tmenier avatar Dec 22 '22 22:12 tmenier