Thursday, 21 May 2020

Autofac, WebApplicationFactory and integration tests

Updated 2nd Oct 2020: for an approach that works with Autofac 6 see this post.


This is one of those occasions where I'm not writing up my own work so much as my discovery after in depth googling.

Integration tests with ASP.NET Core are the best. They spin up an in memory version of your application and let you fire requests at it. They've gone through a number of iterations since ASP.NET Core has been around. You may also be familiar with the TestServer approach of earlier versions. For some time, the advised approach has been using WebApplicationFactory.

What makes this approach particularly useful / powerful is that you can swap out dependencies of your running app with fakes / stubs etc. Just like unit tests! But potentially more useful because they run your whole app and hence give you a greater degree of confidence. What does this mean? Well, imagine you changed a piece of middleware in your application; this could potentially break functionality. Unit tests would probably not reveal this. Integration tests would.

There is a fly in the ointment. A hair in the gazpacho. ASP.NET Core ships with dependency injection in the box. It has its own Inversion of Control container which is perfectly fine. However, many people are accustomed to using other IOC containers such as Autofac.

What's the problem? Well, swapping out dependencies registered using ASP.NET Core's IOC requires using a hook called ConfigureTestServices. There's an equivalent hook for swapping out services registered using a custom IOC container: ConfigureTestContainer. Unfortunately, there is a bug in ASP.NET Core as of version 3.0: When using GenericHost, in tests ConfigureTestContainer is not executed

This means you cannot swap out dependencies that have been registered with Autofac and the like. According to the tremendous David Fowler of the ASP.NET team, this will hopefully be resolved.

In the meantime, there's a workaround thanks to various commenters on the thread. Instead of using WebApplicationFactory directly, subclass it and create a custom AutofacWebApplicationFactory (the name is not important). This custom class overrides the behavior of ConfigureServices and CreateHost with a CustomServiceProviderFactory:

namespace My.Web.Tests.Helpers {
    /// <summary>
    /// Based upon https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/test/integration-tests/samples/3.x/IntegrationTestsSample
    /// </summary>
    /// <typeparam name="TStartup"></typeparam>
    public class AutofacWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class {
        protected override void ConfigureWebHost(IWebHostBuilder builder) {
            builder.ConfigureServices(services => {
                    services.AddSingleton<IAuthorizationHandler>(new PassThroughPermissionedRolesHandler());
                })
                .ConfigureTestServices(services => {
                }).ConfigureTestContainer<Autofac.ContainerBuilder>(builder => {
                    // called after Startup.ConfigureContainer
                });
        }

        protected override IHost CreateHost(IHostBuilder builder) {
            builder.UseServiceProviderFactory(new CustomServiceProviderFactory());
            return base.CreateHost(builder);
        }
    }

    /// <summary>
    /// Based upon https://github.com/dotnet/aspnetcore/issues/14907#issuecomment-620750841 - only necessary because of an issue in ASP.NET Core
    /// </summary>
    public class CustomServiceProviderFactory : IServiceProviderFactory<CustomContainerBuilder> {
        public CustomContainerBuilder CreateBuilder(IServiceCollection services) => new CustomContainerBuilder(services);

        public IServiceProvider CreateServiceProvider(CustomContainerBuilder containerBuilder) =>
        new AutofacServiceProvider(containerBuilder.CustomBuild());
    }

    public class CustomContainerBuilder : Autofac.ContainerBuilder {
        private readonly IServiceCollection services;

        public CustomContainerBuilder(IServiceCollection services) {
            this.services = services;
            this.Populate(services);
        }

        public Autofac.IContainer CustomBuild() {
            var sp = this.services.BuildServiceProvider();
#pragma warning disable CS0612 // Type or member is obsolete
            var filters = sp.GetRequiredService<IEnumerable<IStartupConfigureContainerFilter<Autofac.ContainerBuilder>>>();
#pragma warning restore CS0612 // Type or member is obsolete

            foreach (var filter in filters) {
                filter.ConfigureContainer(b => { }) (this);
            }

            return this.Build();
        }
    }
}

I'm going to level with you; I don't understand all of this code. I'm not au fait with the inner workings of ASP.NET Core or Autofac but I can tell you what this allows. With this custom WebApplicationFactory in play you get ConfigureTestContainer back in the mix! You get to write code like this:

using System;
using System.Net;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Microsoft.Extensions.Options;
using Autofac;
using System.Net.Http;
using Newtonsoft.Json;

namespace My.Web.Tests.Controllers
{
    public class MyControllerTests : IClassFixture<AutofacWebApplicationFactory<My.Web.Startup>> {
        private readonly AutofacWebApplicationFactory<My.Web.Startup> _factory;

        public MyControllerTests(
            AutofacWebApplicationFactory<My.Web.Startup> factory
        ) {
            _factory = factory;
        }

        [Fact]
        public async Task My() {
            var fakeSomethingService = A.Fake<IMySomethingService>();
            var fakeConfig = Options.Create(new MyConfiguration {
                SomeConfig = "Important thing",
                OtherConfigMaybeAnEmailAddress = "johnny_reilly@hotmail.com"
            });

            A.CallTo(() => fakeSomethingService.DoSomething(A<string>.Ignored))
                .Returns(Task.FromResult(true));

            void ConfigureTestServices(IServiceCollection services) {
                services.AddSingleton(fakeConfig);
            }

            void ConfigureTestContainer(ContainerBuilder builder) {
                builder.RegisterInstance(fakeSomethingService);
            }

            var client = _factory
                .WithWebHostBuilder(builder => {
                    builder.ConfigureTestServices(ConfigureTestServices);
                    builder.ConfigureTestContainer<Autofac.ContainerBuilder>(ConfigureTestContainer);
                })
                .CreateClient();

            // Act
            var request = StringContent("{\"sommat\":\"to see\"}");
            request.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
            var response = await client.PostAsync("/something/submit", request);

            // Assert
            response.StatusCode.Should().Be(HttpStatusCode.OK);

            A.CallTo(() => fakeSomethingService.DoSomething(A<string>.Ignored))
                .MustHaveHappened();
        }

    }
}

Sunday, 10 May 2020

From react-window to react-virtual

The tremendous Tanner Linsley recently released react-virtual. react-virtual provides "hooks for virtualizing scrollable elements in React".

I was already using the (also excellent) react-window for this purpose. react-window does the virtualising job and does it very well indeed However, I was both intrigued by the lure of the new shiny thing. I've also never been the biggest fan of react-window's API. So I tried switching over from react-window to react-virtual as an experiment. To my delight, the experiment went so well I didn't look back!

What did I get out of the switch?

  • Simpler code / nicer developer ergonomics. The API for react-virtual allowed me to simplify my code and lose a layer of components.
  • TypeScript support in the box
  • Improved perceived performance. I didn't run any specific tests to quantify this, but I can say that the same functionality now feels snappier.

I tweeted my delight at this and Tanner asked if there was commit diff I could share. I couldn't as it's a private codebase, but I thought it could form the basis of a blogpost.

In case you hadn't guessed, this is that blog post...

Make that change

So what does the change look like? Well first remove react-window from your project:

yarn remove react-window @types/react-window

Add the dependency to react-virtual:

yarn add react-virtual

Change your imports from:

import { FixedSizeList, ListChildComponentProps } from 'react-window';

to:

import { useVirtual } from 'react-virtual';

Change your component code from:

type ImportantDataListProps = {
    classes: ReturnType<typeof useStyles>;
    importants: ImportantData[];
};

const ImportantDataList: React.FC<ImportantDataListProps> = React.memo(props => (
    <FixedSizeList
        height={400}
        width={'100%'}
        itemSize={80}
        itemCount={props.importants.length}
        itemData={props}
    >
        {RenderRow}
    </FixedSizeList>
));

type ListItemProps = {
    classes: ReturnType<typeof useStyles>;
    importants: ImportantData[];
};

function RenderRow(props: ListChildComponentProps) {
    const { index, style } = props;
    const { importants, classes } = props.data as ListItemProps;
    const important = importants[index];

    return (
        <ListItem button style={style} key={index}>
            <ImportantThing classes={classes} important={important} />
        </ListItem>
    );
}

Of the above you can delete the ListItemProps type and the associate RenderRow function. You won't need them again! There's no longer a need to pass down data to the child element and then extract it for usage; it all comes down into a single simpler component.

Replace the ImportantDataList component with this:

const ImportantDataList: React.FC<ImportantDataListProps> = React.memo(props => {
    const parentRef = React.useRef<HTMLDivElement>(null);

    const rowVirtualizer = useVirtual({
        size: props.importants.length,
        parentRef,
        estimateSize: React.useCallback(() => 80, []), // This is just a best guess
        overscan: 5
    });

    return (
            <div
                ref={parentRef}
                style={{
                    width: `100%`,
                    height: `500px`,
                    overflow: 'auto'
                }}
            >
                <div
                    style={{
                        height: `${rowVirtualizer.totalSize}px`,
                        width: '100%',
                        position: 'relative'
                    }}
                >
                    {rowVirtualizer.virtualItems.map(virtualRow => (
                        <div
                            key={virtualRow.index}
                            ref={virtualRow.measureRef}
                            className={props.classes.hoverRow}
                            style={{
                                position: 'absolute',
                                top: 0,
                                left: 0,
                                width: '100%',
                                height: `${virtualRow.size}px`,
                                transform: `translateY(${virtualRow.start}px)`
                            }}
                        >
                            <ImportantThing
                                classes={props.classes}
                                important={props.importants[virtualRow.index]}
                            />
                        </div>
                    ))}
                </div>
            </div>
    );
});

And you are done! Thanks Tanner for this tremendous library!