Using custom startup class with ASP.NET Core integration tests

My previous post demonstrated how to use custom appsettings.js file with integration tests in ASP.NET Core. But in practice it’s not enough and very often we need custom startup class that extends the one in web application project to configure application for integration tests. This blog post shows how to do it.

Getting started

Using custom startup class is a little bit tricky. I start again with simple classic integration test from ASP.NET Core integration testing document.

public class HomeControllerTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _factory;

    public HomeControllerTests(WebApplicationFactory<Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

Making members of Startup class virtual

To avoid all kind of confusion with method calls we have to make the methods in web application startup class virtual.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public virtual void ConfigureServices(IServiceCollection services)
    {
        // Configure services
        // Configure dependency injection
    }

    public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        // Configure application
    }
}

If there’s only one method to override then other methods in Startup class can be non-virtual.

Adding custom startup class

Now let’s create custom startup class to integration tests project. I call it FakeStartup. It extends Startup class of web application and overrides Configure() method.

public class FakeStartup : Startup
{
    public FakeStartup(IConfiguration configuration) : base(configuration)
    {
    }

    public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        base.Configure(app, env, loggerFactory);

        var serviceScopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
        using (var serviceScope = serviceScopeFactory.CreateScope())
        {
            var dbContext = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();

            if (dbContext.Database.GetDbConnection().ConnectionString.ToLower().Contains("database.windows.net"))
            {
                throw new Exception("LIVE SETTINGS IN TESTS!");
            }

            // Initialize database
        }
    }
}

I didn’t add much logic to Configure() method of FakeStartup class. There’s just one check to make sure that SQL Server connection string doesn’t point to live database. And there’s also room left for database initialization.

Custom web application factory

To make test host use our fake startup class with web application we need to apply some magic. First we need custom web application factory that provides integration tests mechanism with custom web host builder. Here is my application factory – dummy and very general.

public class MediaGalleryFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
    protected override IWebHostBuilder CreateWebHostBuilder()
    {
        return WebHost.CreateDefaultBuilder(null)
                      .UseStartup<TEntryPoint>();
    }
}

NB! Configuration built for web host in method above must contain same services as specified in program.cs file of web application. To avoid synchronizing changes between web host builders in web application and integration tests project use some general method for web host configuring or move configuration to Startup class.

Configuring integration test

Using this custom web application factory doesn’t come for free. We have to solve some issues when switching over to fake startup class:

  1. Custom location for web host builder confuses integration tests mechanism and we have to point out the correct location of web application content root (this is the folder of web application).
  2. Related to previous point we have to tell that web application assembly is the one where application parts like controllers, views etc must be searched.

Simple integration test in the beginning of this post is here with support for custom startup class.

public class HomeControllerTests : IClassFixture<MediaGalleryFactory<FakeStartup>>
{
    private readonly WebApplicationFactory<FakeStartup> _factory;

    public HomeControllerTests(MediaGalleryFactory<FakeStartup> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.UseSolutionRelativeContentRoot("MediaGallery");

            builder.ConfigureTestServices(services =>
            {
                services.AddMvc().AddApplicationPart(typeof(Startup).Assembly);
            });
               
        });
    }

    [Theory]
    [InlineData("/")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

Now we are done with custom startup class but let’s stilll get further.

Using custom appsettings.js for integration tests

When I run this test I get the following error. Remember what we did in custom startup class?

Integration test failed because of live settings

To solve this issue we have to go back to my previous blog post Using custom appsettings.json with ASP.NET Core integration tests. We need appsettings.json file where database connection string and other settings are defined for integration tests.

After introducing appsettings.json for integration tests our test class is complete.

public class HomeControllerTests : IClassFixture<MediaGalleryFactory<FakeStartup>>
{
    private readonly WebApplicationFactory<FakeStartup> _factory;

    public HomeControllerTests(MediaGalleryFactory<FakeStartup> factory)
    {
        var projectDir = Directory.GetCurrentDirectory();
        var configPath = Path.Combine(projectDir, "appsettings.json");

        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.UseSolutionRelativeContentRoot("MediaGallery");

            builder.ConfigureAppConfiguration(conf =>
            {
                conf.AddJsonFile(configPath);
            });

            builder.ConfigureTestServices(services =>
            {
                services.AddMvc().AddApplicationPart(typeof(Startup).Assembly);
            });               
        });
    }

    [Theory]
    [InlineData("/")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

Wrapping up

Custom startup class for integration tests may be useful when integration tests need additional configuring and we need to do it when web application is configuring. My simple custom startup class demonstrated how to fail tests when integration tests are using live configuration. It’s also possible to use custom startup class to configure services using integration tests specific options. Getting custom startup class work was challenging but it’s actually easy once it’s done as the amount of new code was small.

Liked this post? Empower your friends by sharing it!

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    Leave a Reply

    Your email address will not be published. Required fields are marked *