Michael Freidgeim
Michael Freidgeim

Reputation: 28501

Can I use FlurlClient with Asp.Net Core TestServer?

We are using FlurlClient in a few projects and familiar with their fluent interface. We now want to use it in asp.net core integration tests using TestServer. The example from http://asp.net-hacker.rocks/2017/09/27/testing-aspnetcore.html

_server = new TestServer(new WebHostBuilder()
                             .UseStartup<Startup>());
_client = _server.CreateClient();

I was going to change code to

_server = new TestServer(new WebHostBuilder()
                             .UseStartup<Startup>());
var httpClient = _server.CreateClient();
_client = new FlurlClient(httpClient);

and use all FlurlClient methods/extensions.

But then I noticed Is it possible to use Furl.Http with the OWIN TestServer? which described that more work is required in owin implementation.

Is approach for Asp.Net Core TestServer similar? Or is it simplified?

Upvotes: 2

Views: 1461

Answers (4)

Andrew
Andrew

Reputation: 8673

Updating my factory based on this github issue with Flurl worked for me, and is hands-off in the test class itself.

  • Flurl 4.0.2
  • dotnet 8
// uses Program.cs in Api/ project
public sealed class MyTestFactory : WebApplicationFactory<Program>
{
    public MyTestFactory() {
        Server.PreserveExecutionContext = true;
    }
}

And then using the http client in my xUnit tests worked the same as when calling the controllers directly for unit testing.

public class TestLiveServer(MyTestFactory factory) : IClassFixture<MyTestFactory>
{
    [Fact]
    public async Task test_the_live_output_is_right()
    {
        using var http = new HttpTest();
        http.RespondWith("content-read-from-a-file.json"));

        var client = factory.CreateClient();
        var response = await client.GetAsync("/api/v7/users");

Upvotes: 0

ds99jove
ds99jove

Reputation: 638

I did not quite get the solution from Lennart to work, but it did set me on the right path. Unfortunately I think the whole solution is very hack-ish and I'd much prefer the solution from Flurl 3.x. Not sure why it's no longer possible to inject my own IHttpClient when using clientless pattern like before.

I ended up doing 2 changes from Lennarts answer

First


    public class TestServerMessageHandler : DelegatingHandler
    {
        private readonly TestServer _factoryServer;

        public TestServerMessageHandler(TestServer factoryServer) : base()
        {
            _factoryServer = factoryServer;
        }
        
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (InnerHandler is HttpClientHandler)
            {
                InnerHandler?.Dispose();
                InnerHandler = _factoryServer.CreateHandler();                
            }

            return base.SendAsync(request, cancellationToken);
        }
    }

If the InnerHandler is recreated it will throw an error:

"Call failed. This instance has already started one or more requests. Properties can only be modified before sending the first request"

Therefor I check the type of InnerHandler since that changes after it's been recreated once.

Secondly I need to start a new factory for each test to reset some conditions from previous tests. Using NUnit my code looks like this (Setup runs for each test)

       [SetUp]
        public void Setup()
        {
            _factory = new CustomWebApplicationFactory<Startup>();

            FlurlHttp.Clients.Clear(); // Needs to be done each test so we can add a new middleware for current WebApplicationFactory

            FlurlHttp
                .ConfigureClientForUrl("http://localhost")
                .ConfigureHttpClient(client => client.BaseAddress = _factory.ClientOptions.BaseAddress)
                .AddMiddleware(() => new TestServerMessageHandler(_factory.Server));

            _httpClient = _factory.CreateClient();

            // More stuff
        }

Upvotes: 1

Lennart
Lennart

Reputation: 10343

For Flurl 4.x the solution looks a bit differently due to API changes if you don't want to use an explicit FlurlClient, but rather the clientless approach. Assuming you have a WebApplicationFactory:

var webApplicationFactory = new WebApplicationFactory<Startup>()
     .WithWebHostBuilder(
         builder =>
         {
             // Your configuration 
         });

You need to copy at least the base address to the Flurl and add a custom middleware that "redirects" the HTTP calls to the test server:

 FlurlHttp
     .ConfigureClientForUrl(string.Empty) // Omit or change if you're using different URIs in your tests
     .ConfigureHttpClient(client => client.BaseAddress = webApplicationFactory.ClientOptions.BaseAddress)
     .AddMiddleware(() => new TestServerMessageHandler(webApplicationFactory.Server));

Where TestServerMessageHandler is:

private class TestServerMessageHandler(Microsoft.AspNetCore.TestHost.TestServer testServer) : DelegatingHandler
 { 
     protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         InnerHandler?.Dispose();
         InnerHandler = testServer.CreateHandler();
         return base.SendAsync(request, cancellationToken);
     }
 }

InnerHandler is the Flurl handler, since we want to use a handler from our test server, we need to dispose and replace it.

This enables you to make calls like "/v0/test".GetJsonAsync<SomeDto>() in your tests, that Flurl will execute with the test server.

Upvotes: 3

Todd Menier
Todd Menier

Reputation: 39349

It's much simplified, and your proposed change is exactly right. The question you linked to is old and my answer contains information that's no longer relevant in 2.x. (I have updated it.) In fact, the ability to provide an existing HttpClient directly in a FlurlClient constructor was added very recently, and with this specific use case in mind.

Here's an extension method I use as a replacement for CreateClient; you might find it handy if you do this a lot:

public static class TestServerExtensions
{
    public static IFlurlClient CreateFlurlClient(this TestServer server) => 
        new FlurlClient(server.CreateClient());
}

Upvotes: 6

Related Questions