Configure Integration Test (.Net Core)

Spread the love

Steps are tested on the .Net 8

The steps are:

  1. Add a xUnit project to .Net core solution.
  2. Using WebApplicationFactory, we can configure the services. Example is below.
  3. Use TestContainers for MSSQL, because we can’t touch the dev/prod DBs for testing. The example of WebApplicationFactory has the lines to configure the MSSQL test container. Please make user that Docker is installed and try to use the WSL if you can for container running (Choice given on docker installation).
  4. Now we can create a new Test class and write our test cases.

Nuget Packages Needed:

  1. Testcontainers
  2. xunit.runner.visualstudio
  3. microsoft.aspnetcore.mvc.testing
  4. microsoft.data.sqlclient
  5. microsoft.net.test.sdk
  6. testcontainers.mssql
  7. Xunit.Extensions.Ordering
  8. Don’t remove any other that are by default installed while adding xUnit project 🙂

Example code:

WebApplicationFactory:

public class TestingWebAppFactory<TEntryPoint> : WebApplicationFactory<TEntryPoint> where TEntryPoint : Program
{
    private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder()
        .WithImage("mcr.microsoft.com/mssql/server:2022-latest") // Image from: https://hub.docker.com/_/microsoft-mssql-server
        .Build();
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove the real DB configuration
            var dbContextDescriptor = services
                                       .FirstOrDefault(s => s.ServiceType == typeof(DbContextOptions<YourDbContext>));

            if (dbContextDescriptor != null)
            {
                services.Remove(dbContextDescriptor);
            }

            // start the container and update MSSQL
            // This may timeout. So, to fix this you can create your wait strategy : https://dotnet.testcontainers.org/api/wait_strategies/
            _msSqlContainer.StartAsync().GetAwaiter().GetResult();
            var connectionString = _msSqlContainer.GetConnectionString();
            services.AddDbContext<YourDbContext>((options) => options.UseSqlServer(connectionString));

            var serviceProvider = services.BuildServiceProvider();

            using var scope = serviceProvider.CreateScope();
            using var dbContext = scope.ServiceProvider.GetRequiredService<YourDbContext>();
            try
            {
                var created = dbContext.Database.EnsureCreated();
            } catch (Exception e)
            {
                Console.WriteLine($"Error in TestingWebAppFactory");
                throw new Exception(e.StackTrace);
            }
        });
        base.ConfigureWebHost(builder);
    }

    // To stop the containers
    public Task DisposeSqlContainer()
    => _msSqlContainer.DisposeAsync().AsTask();
}

If Program is not found, then you can create a partial Program class at the end of Program.cs of your main project.

public partial class Program { }

Test Case Class

public class ControllerIntegrationTest : IClassFixture<TestingWebAppFactory<Program>>, IAsyncLifetime
{
    private readonly TestingWebAppFactory<Program> _factory;
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl = $"/api/v1.0/{nameof(YourController).Replace("Controller", "")}";

    public ControllerIntegrationTest(TestingWebAppFactory<Program> factory)
    {
        _factory = factory;
        _httpClient = _factory.CreateClient();
    }
    public Task DisposeAsync()
    {
        return _factory.DisposeSqlContainer();
    }

    public Task InitializeAsync()
    {
        return Task.CompletedTask;
    }

    [Theory]
    [InlineData("/")]
    public async Task Your_Controller_Test(string path)
    {
        var model = new YourCreateRequestModel()
        {
            Name = "Test",
        };

        var httpContent = new StringContent(JsonConvert.SerializeObject(model));
        httpContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        var completePath = _baseUrl + path;

        var createdResponse = await _httpClient.PostAsync(completePath, httpContent);

        createdResponse.EnsureSuccessStatusCode();

    }
}

Errors:

  1. Docker error:
System.AggregateException : One or more errors occurred. (Docker is either not running or misconfigured. Please ensure that Docker is running and that the endpoint is properly configured. You can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. For more information, visit:
 https://dotnet.testcontainers.org/custom_configuration/ (Parameter 'DockerEndpointAuthConfig')) (The following constructor parameters did not have matching fixture data: TestingWebAppFactory`1 factory)
 ---- System.ArgumentException : Docker is either not running or misconfigured. Please ensure that Docker is running and that the endpoint is properly configured. You can customize your configuration using either the environment variables or the ~/.testcontainers.properties file. For more information, visit:
 https://dotnet.testcontainers.org/custom_configuration/ (Parameter 'DockerEndpointAuthConfig')

2. Timeout Error: You may face timeout error. So, you can add wait strategy (Read article again 🙂 You missed it. Including code)

Till now well and good. Now answer the following:

  1. What if your test cases depend on each other. E.g. you create a record and next test case need that record for editing so on and so forth.
  2. Also, we don’t want to create a new instance of database container every time a new test case is class is run.
  3. What about ordering of the test cases. e.g. first you want to create a record and then fetch it.

For the single database creation, you can use Singleton pattern or assembly level fixture (available in xUnit v3). I have used singleton pattern:

/// <summary>
    /// Database will shared accross the Contexts
    /// </summary>
    public sealed class DBContainerSingleton : IDisposable
    {
        private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest") // Image from: https://hub.docker.com/_/microsoft-mssql-server
            .Build();
        private static DBContainerSingleton _containerInstance = null;
        private static readonly object _padlock = new object();

        public static DBContainerSingleton Instance
        {
            get
            {
                if (_containerInstance == null)
                {
                    lock (_padlock)
                    {
                        if (_containerInstance == null)
                        {
                            _containerInstance = new DBContainerSingleton();
                        }
                    }
                }
                return _containerInstance;
            }
        }

        public async Task StartMSSQLServer()
        {
            if (_msSqlContainer.State != TestcontainersStates.Running || _msSqlContainer.State != TestcontainersStates.Restarting)
            {
                await _msSqlContainer.StartAsync();
            }
        }

        public string GetConnectionString()
        {
            // Update the DB name here.
            var dbConnectionString = this._msSqlContainer.GetConnectionString();
            return dbConnectionString.Replace("master", "TestDB");
        }

        void IDisposable.Dispose()
        {
            _msSqlContainer.DisposeAsync();
        }
    }

Sample connection string for docker deployed SQL server:

Server=127.0.0.1,52901;Database=master;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True

For context sharing I have used the ICollectionFixture. You can also use IClassFixture, if you only want to share the fixture in only one test case class. E.g. of ICollectionFixture:

 [CollectionDefinition("ACollection")]
    public class TestingWebAppFactoryCollection : ICollectionFixture<TestingWebAppFactory<Program>>
    {
        // This class has no code, and is never created. Its purpose is simply
        // to be the place to apply [CollectionDefinition] and all the
        // ICollectionFixture<> interfaces.
    }

// Use the above definition

    [Collection("ACollection")]
    public class CarrierControllerIntegrationTest
    {
        private readonly TestingWebAppFactory<Program> _factory;
        private readonly HttpClient _httpClient;
       
        public CarrierControllerIntegrationTest(TestingWebAppFactory<Program> factory)
        {
            _factory = factory;
            _httpClient = _factory.CreateClient();
        }
       // your test cases
     }

For ordering used the Xunit.Extensions.Ordering nuget package. Link for more: GitHub – tomaszeman/Xunit.Extensions.Ordering: Xunit extension with full support for ordered testing at collection, class and test case levels. Full-featured AssemblyFixture implementation.

Cheers and Peace out!!!