Skip to content

BrowsR helper for integration testing in ASP.NET 5

January 1, 2016

Dominick and I were working together on some unit tests for an ASP.NET 5 project. These tests were integration tests so we used the the TestServer from ASP.NET 5 TestHost project to load an entire ASP.NET pipeline. This allows you to use HttpClient to make HTTP calls into the running server. The main problem we had was that we needed to simulate a browser client that could hold onto cookies and follow redirects, so based upon Damian Hickey’s excellent OwinMessageHandler, we crufted up this for ASP.NET 5 (and of course had to give it a witty name):

public class BrowsR : DelegatingHandler
{
    private CookieContainer _cookieContainer = new CookieContainer();

    public bool AllowAutoRedirect { get; set; } = true;
    public bool AllowCookies { get; set; } = true;
    public int AutoRedirectLimit { get; set; } = 20;

    public BrowsR(HttpMessageHandler next)
        : base(next)
    {
    }

    protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await SendCookiesAsync(request, cancellationToken);

        int redirectCount = 0;

        while (AllowAutoRedirect && (
                response.StatusCode == HttpStatusCode.Moved
                || response.StatusCode == HttpStatusCode.Found))
        {
            if (redirectCount >= AutoRedirectLimit)
            {
                throw new InvalidOperationException(string.Format("Too many redirects. Limit = {0}", redirectCount));
            }
            var location = response.Headers.Location;
            if (!location.IsAbsoluteUri)
            {
                location = new Uri(response.RequestMessage.RequestUri, location);
            }

            request = new HttpRequestMessage(HttpMethod.Get, location);

            response = await SendCookiesAsync(request, cancellationToken).ConfigureAwait(false);

            redirectCount++;
        }
        return response;
    }

    protected async Task<HttpResponseMessage> SendCookiesAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (AllowCookies)
        {
            string cookieHeader = _cookieContainer.GetCookieHeader(request.RequestUri);
            if (!string.IsNullOrEmpty(cookieHeader))
            {
                request.Headers.Add("Cookie", cookieHeader);
            }
        }

        var response = await base.SendAsync(request, cancellationToken);

        if (AllowCookies && response.Headers.Contains("Set-Cookie"))
        {
            var responseCookieHeader = string.Join(",", response.Headers.GetValues("Set-Cookie"));
            _cookieContainer.SetCookies(request.RequestUri, responseCookieHeader);
        }

        return response;
    }
}
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

To use it, then you’d do something like this:

var startup = new Startup();
var server = TestServer.Create(null, _startup.Configure, startup.ConfigureServices);
var handler = server.CreateHandler();
var client = new HttpClient(new BrowsR(handler));
var result = await client.GetAsync("...");

The requests will now follow redirects and hold onto cookies across those redirects. Too bad this isn’t built into the ASP.NET TestHost itself.

Enjoy.

6 Comments leave one →
  1. January 4, 2016 4:13 am

    Immer wieder erstaunlich an was für Details man scheitert:
    Wie erstellt man die _Startup-Klasse? bzw, was geb ich dem Konstruktor als IHostingEnvironment um die zu erschaffen?

    • January 5, 2016 4:47 pm

      Sorry I don’t speak German, but I suspect you’re asking for the Startup class. I’ll update the post with it.

      • January 6, 2016 5:33 am

        Oh, sorry, I sometimes don’t realise what language I’m reading :)
        I was not able to create and use the Startup-class like you do, it needs Parameters for the constructor (IHostingEnvironment) and for the configure/configureservices methods (logging, db-seeder and more).
        I found this solution, which works:
        TestServer = new TestServer(TestServer.CreateBuilder().UseStartup());

        Now my problem is: MVC does not work when the tests reside in a different project/assembly than the views/viewmodels. Running the tests inside the asp.net5 project works, but as soon as I put the tests into the separate unittest-assembly MVC can’t find the views anymore. It Shows an error like this:
        Bsoft.Buchhaltung.Tests.LoginTests.SomeTest [FAIL]
        System.InvalidOperationException : The view ‘About’ was not found. The following locations were searched:
        /Views/Home/About.cshtml
        /Views/Shared/About.cshtml.

  2. January 12, 2016 4:14 pm

    Hi Brock,
    Will your Workshop at DEV-intersection 2016 go over IDServer 4 and vNext?
    Will they be hands-on sessions?
    Looking forward to seeing you in Florida!

    • January 24, 2016 8:14 pm

      It will cover the general features and how to setup IdSvr — both 3 and 4 are generally the same.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: