In software development, testing is an essential aspect that ensures the stability and reliability of applications. Three primary types of tests are commonly used: unit tests, integration tests, and end-to-end tests. In this blog post, we will discuss these testing types in the context of a .NET WebAPI project and provide an example implementation of integration testing using an in-memory database.


Unit Tests, Integration Tests, and End-to-End Tests: What’s the Difference?

  • Unit tests focus on testing individual units or components of your application in isolation. This type of test verifies whether the unit/component works correctly and adheres to its business logic.
  • Integration tests aim to test multiple units or components together, ensuring that they interact properly. Integration tests can uncover issues related to data flow, communication between components, and external dependencies like databases.
  • End-to-end (E2E) tests simulate a complete user scenario by testing the entire application from start to finish. E2E tests can help identify issues related to multiple components, external APIs, and user interfaces.

Example Implementation in .NET

To perform integration tests, we will use an in-memory database and the WebApplicationFactory feature of ASP.NET Core. This approach allows us to test our WebAPI application with a real web server in memory, simulating how the components interact with each other when making requests.

First, let’s create an InMemoryDbAppFactory that sets up our in-memory web server and database:

public class InMemoryDbAppFactory : WebApplicationFactory<Program>
{
    private readonly string _environment;

    public InMemoryDbAppFactory()
    {
        _environment = "IntegrationTests";
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", _environment);
        builder.UseEnvironment(_environment);
        builder.UseSetting("https_port", "8080");

        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll<ITestRepository>();
            services.TryAddTransient<ITestRepository, TestRepositoryTest>();

            // Anonymous authentication
            services
                .AddAuthentication("Test")
                .AddScheme<AuthenticationSchemeOptions, TestAuthenticationHandler>("Test", options => { });
        });
    }
}

In order to use an in-memory database, we will override the connection string injection in our ServiceCollectionExtensions:

public static partial class ServiceCollectionExtensions
{
    public static IServiceCollection AddDbContexts(
        this IServiceCollection services,
        IConfiguration configuration,
        IWebHostEnvironment environment
    )
    {
        if (environment.IsEnvironment("IntegrationTests"))
        {
            var connDb = new SqliteConnection("DataSource=db;mode=memory;cache=shared");
            connDb.Open();
            services.AddDbContext<dbContext>(options => options.UseSqlite(connDb));
        }
        else
        {
            services.AddDbContext<dbContext>(
                options => options.UseSqlServer(configuration.GetConnectionString("ConnectionString")).UseExceptionProcessor(),
                ServiceLifetime.Scoped
            );
        }

        return services;
    }
}

Finally, this is how a test looks like:

[Collection("MemoryDbIntegrationTests")]
public class ApiTests : IClassFixture<InMemoryDbAppFactory>
{
    private readonly InMemoryDbAppFactory _factory;
    private readonly HttpClient _client;

    public ApiTests(InMemoryDbAppFactory factory)
    {
        _factory = factory;
        _client = factory.CreateClient();

        // Ensures a clean database before each test
        factory.ResetDb();
    }

    [Fact]
    public async Task GetList_Should_Return_List()
    {
        // Arrange

        // Act
        var response = await _client.GetAsync("/api/list");
        var content = await response.Content.ReadFromJsonAsync<IEnumerable<Dto>>();

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        content.Should().NotBeNull();
        content.Should().HaveCount(2);
    }
}


Using an in-memory database for integration tests has both advantages and disadvantages:

Advantages:

  • Faster test execution: In-memory databases provide quicker test execution times since the data is stored in memory instead of on disk. This can significantly improve your overall testing performance and help you find issues faster.
  • Easy to set up and tear down: In-memory databases are easy to create, modify, and delete without the need for external dependencies or configuration changes. This makes it easier to write, run, and maintain your tests.
  • Consistent test data: In-memory databases allow you to create a known set of data for your tests, ensuring that each test starts with the same initial conditions. This can help reduce the likelihood of inconsistent test results and make it easier to identify issues related to data flow or dependencies between tests.

Disadvantages:

  • Limited scalability: In-memory databases have limited capacity and may not be suitable for testing large datasets or complex scenarios that require high levels of concurrency.
  • Lack of realism: In-memory databases may not accurately represent the behavior or performance of a production database, especially when dealing with complex queries or data modification operations. This can make it difficult to identify issues related to database schema, indexing, and query optimization.
  • Limited support for advanced features: using SQLite as our in memory database we don’t have support for multiple schemas and some advanced query features.
  • Seed data: the need to create seed data can be tedious and time consuming.

When deciding whether to use an in-memory database, mock data, or a real web server in memory for your integration tests, consider the specific requirements of your project and weigh the advantages and disadvantages of each approach. In many cases, using a combination of testing types and approaches can help you ensure the stability, reliability, and performance of your .NET WebAPI application.

LEAVE A REPLY

Please enter your comment!
Please enter your name here