Using a Custom Startup Class With ASP.NET Core Integration Tests

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

Getting Started

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

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 the Startup Class Virtual

To avoid all kinds of confusion with method calls, we have to make the methods in the web application's 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 the Startup class can be non-virtual.

Adding a Custom Startup Class

Now let's create a custom startup class for the integration tests project. I call it FakeStartup. It extends the Startup class of our web application and overrides the 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 the  Configure() method of the FakeStartup class. There's just one check to make sure that the SQL Server connection string doesn't point to a live database. And there's also room left for database initialization.

Custom Web Application Factory

To make the test host use our fake startup class with the web application we need to apply some magic. First, we need a custom web application factory that provides integration test mechanisms with a custom web host builder. Here is my application factory - fairly simple and very general.

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

NB! The configuration built for a web host in the method above must contain the same services as specified in the program.cs file of the web application. To avoid synchronizing changes between web host builders in web applications and integration test projects, we can use some general methods for configuring web hosts or moving the  configuration to the Startup class.

Configuring the Integration Test

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

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

The simple integration test form the beginning of this post is given below, only now with support for the 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 go a little further.

Using a Custom appsettings.js File for Integration Tests

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

Integration Tests

To solve this issue we have to go back to my previous blog post using a custom appsettings.json file with ASP.NET Core integration tests. We need the appsettings.json file where the 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

A 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 a custom startup class to configure services using integration test-specific options. Getting a custom startup class to work was challenging but it's actually easy once it's done, as the amount of new code is small.

 

 

 

 

Top