Table of Contents

Integration Testing

Integration tests in the template focus on verifying real application flows by wiring services, infrastructure, and data access together.

What DevKit already provides

The DevKit testing library provides IntegrationTestsBase in IntegrationTestsBase.cs.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

using Microsoft.Extensions.DependencyInjection;

namespace CodeBlock.DevKit.Test.TestBase;

/// <summary>
/// Base abstract class for integration test classes that provides database and service management.
/// Manages test database lifecycle and dependency injection container for integration testing scenarios.
/// Supports different fixture sharing strategies for test isolation and performance optimization.
/// </summary>
/// <remarks>
/// Integration tests require database access and external dependencies. This base class provides:
/// - Unique database per test fixture to prevent test interference
/// - Service provider management for dependency injection
/// - Automatic database cleanup after test execution
/// - Support for different fixture sharing strategies (per test, per class, or per collection)
/// </remarks>

public abstract class IntegrationTestsBase : IDisposable
{
    /// <summary>
    /// Service provider instance for dependency injection during tests.
    /// Provides access to registered services like repositories, services, and database contexts.
    /// </summary>
    protected readonly IServiceProvider _serviceProvider;

    /// <summary>
    /// Initializes a new instance of IntegrationTestsBase with a unique database suffix.
    /// Creates a new service provider and initializes the test database.
    /// </summary>
    /// <param name="dbNameSuffix">Unique identifier for the test database to prevent conflicts between parallel test runs.</param>

    public IntegrationTestsBase(string dbNameSuffix)
    {
        _serviceProvider = GetServiceProvider(dbNameSuffix);
        InitialDatabase();
    }

    /// <summary>
    /// Abstract method to initialize the test database with required schema and seed data.
    /// Called during construction to set up the test environment before test execution.
    /// </summary>

    public abstract void InitialDatabase();

    /// <summary>
    /// Abstract method to clean up the test database after test execution.
    /// Called during disposal to ensure test isolation and prevent data leakage.
    /// </summary>

    public abstract void DropDatabase();

    /// <summary>
    /// Abstract method to create and configure the service provider for dependency injection.
    /// Should return a configured IServiceProvider with all required services registered.
    /// </summary>
    /// <param name="dbNameSuffix">Database suffix to configure unique connection strings.</param>
    /// <returns>Configured service provider for the test.</returns>

    public abstract IServiceProvider GetServiceProvider(string dbNameSuffix);

    /// <summary>
    /// Retrieves a required service from the dependency injection container.
    /// Throws an exception if the service is not registered, ensuring test setup correctness.
    /// </summary>
    /// <typeparam name="T">The type of service to retrieve.</typeparam>
    /// <returns>The requested service instance.</returns>

    protected T GetRequiredService<T>()
    {
        return _serviceProvider.GetRequiredService<T>();
    }

    /// <summary>
    /// Disposes the test fixture and cleans up the test database.
    /// Ensures proper cleanup of resources and test isolation.
    /// </summary>
    public void Dispose()
    {
        DropDatabase();
    }
}

This base class is designed to help you implement:

  • Service-provider-based test setup
  • Database initialization and cleanup lifecycle
  • Reusable helper access through GetRequiredService<T>()

Template fixture example

The template integration fixture implementation is in TestsBaseFixture.cs.

using AutoMapper;
using CanBeYours.Core.Domain.DemoThings;
using CanBeYours.Infrastructure;
using CodeBlock.DevKit.Application.Srvices;
using CodeBlock.DevKit.Clients.AdminPanel;
using CodeBlock.DevKit.Contracts.Services;
using CodeBlock.DevKit.Test.TestBase;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NSubstitute;

namespace CanBeYours.Application.Tests.Integration.Fixtures;

/// <summary>
/// Base test fixture for integration tests that provides common setup and utilities.
/// This class demonstrates how to create a robust test infrastructure for integration testing
/// with dependency injection, database setup, and mock services. The DemoThing functionality
/// shown here is just an example to help you learn how to implement your own unique features
/// into the current codebase. You can use this pattern to test your actual business logic.
/// </summary>
public abstract class TestsBaseFixture : IntegrationTestsBase
{
    public readonly IRequestDispatcher _requestDispatcher;
    public readonly IMapper _mapper;
    public readonly IDemoThingRepository _demoThingRepository;
    public readonly ICurrentUser _currentUser;
    public readonly IUserAccessorService _userAccessorService;

    /// <summary>
    /// Initializes the test fixture with a unique database suffix and sets up common services.
    /// This constructor demonstrates how to configure test dependencies and mock services
    /// for integration testing scenarios.
    /// </summary>
    /// <param name="dbNameSuffix">Unique suffix for the test database to ensure test isolation</param>
    protected TestsBaseFixture(string dbNameSuffix)
        : base(dbNameSuffix)
    {
        _demoThingRepository = GetRequiredService<IDemoThingRepository>();
        _userAccessorService = GetRequiredService<IUserAccessorService>();
        _mapper = GetRequiredService<IMapper>();
        _requestDispatcher = Substitute.For<IRequestDispatcher>();
        _currentUser = Substitute.For<ICurrentUser>();
        _currentUser.GetUserId().Returns(Guid.NewGuid().ToString());
    }

    /// <summary>
    /// Initializes the test database. Override this method if you need custom database setup.
    /// This method is called by the base class to prepare the database for testing.
    /// </summary>
    public override void InitialDatabase() { }

    /// <summary>
    /// Drops the test database after each test run to ensure clean test isolation.
    /// This method is used in the base class to clean up resources between tests.
    /// </summary>
    public override void DropDatabase()
    {
        _serviceProvider.DropTestDatabase();
    }

    /// <summary>
    /// Seeds the test database with a DemoThing entity for testing purposes.
    /// This method demonstrates how to prepare test data for integration tests.
    /// Example usage: await fixture.SeedDemoThingAsync(demoThing);
    /// </summary>
    /// <param name="demoThing">The DemoThing entity to seed in the test database</param>
    public async Task SeedDemoThingAsync(DemoThing demoThing)
    {
        await _demoThingRepository.AddAsync(demoThing);
    }

    /// <summary>
    /// Retrieves a DemoThing entity from the test database by its ID.
    /// This method is useful for verifying that entities were properly created or updated
    /// during test execution.
    /// Example usage: var result = await fixture.GetDemoThingAsync(entityId);
    /// </summary>
    /// <param name="id">The unique identifier of the DemoThing to retrieve</param>
    /// <returns>The DemoThing entity if found, null otherwise</returns>
    public async Task<DemoThing> GetDemoThingAsync(string id)
    {
        return await _demoThingRepository.GetByIdAsync(id);
    }

    /// <summary>
    /// Gets a required service from the test service provider.
    /// This method provides access to the configured services for testing purposes.
    /// </summary>
    /// <typeparam name="T">The type of service to retrieve</typeparam>
    /// <returns>The requested service instance</returns>
    public new T GetRequiredService<T>()
    {
        return _serviceProvider.GetRequiredService<T>();
    }

    /// <summary>
    /// Configures and builds the test service provider with all necessary dependencies.
    /// This method demonstrates how to set up a complete dependency injection container
    /// for integration testing, including database configuration, logging, and module registration.
    /// </summary>
    /// <param name="dbNameSuffix">Unique suffix for the test database name</param>
    /// <returns>A configured service provider ready for testing</returns>
    public override IServiceProvider GetServiceProvider(string dbNameSuffix)
    {
        var builder = WebApplication.CreateBuilder();

        // Configure the test configuration
        builder
            .Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile("appsettings.Development.json", optional: true)
            .AddInMemoryCollection([new("MongoDB:DatabaseName", $"Test_DemoThings_DB_{dbNameSuffix}")]);

        // Add logging services
        builder.Services.AddLogging(logging =>
        {
            logging.AddConsole();
            logging.SetMinimumLevel(LogLevel.Debug);
        });

        // Register AdminPanel client module
        builder.AddAdminPanelClientModule(typeof(TestsBaseFixture));

        // Register infrastructure services first
        builder.Services.AddInfrastructureModule();

        // Build the service provider
        return builder.Services.BuildServiceProvider();
    }
}

The collection fixture wiring is in DemoThingsCollectionFixture.cs.

namespace CanBeYours.Application.Tests.Integration.Fixtures;

/// <summary>
/// Collection definition for DemoThings integration tests that enables test isolation and shared fixture usage.
/// 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. This pattern demonstrates
/// how to organize integration tests that share common setup and teardown logic.
/// The DemoThing functionality shown here is just an example to help you learn how to implement
/// your own unique features into the current codebase.
/// </summary>
[CollectionDefinition(nameof(DemoThingsCollectionFixture))]
public class DemoThingCollectionFixtureDefinition : ICollectionFixture<DemoThingsCollectionFixture>
{
    // 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.
}

/// <summary>
/// Collection fixture for DemoThings integration tests that provides shared test infrastructure.
/// This class extends TestsBaseFixture to create a dedicated test collection with its own database
/// instance, ensuring test isolation while allowing tests within the collection to share common setup.
/// The DemoThing functionality shown here is just an example to help you learn how to implement
/// your own unique features into the current codebase. You can use this pattern to organize
/// tests for your actual business entities.
/// </summary>
public class DemoThingsCollectionFixture : TestsBaseFixture
{
    /// <summary>
    /// Initializes the DemoThings collection fixture with a unique database suffix.
    /// This constructor ensures that all tests in this collection use the same database instance,
    /// allowing for shared test data and proper cleanup between test runs.
    /// </summary>
    public DemoThingsCollectionFixture()
        : base(dbNameSuffix: nameof(DemoThingsCollectionFixture)) { }
}

One complete test example

For a complete integration test example, review UpdateDemoThingTests.cs.

using CanBeYours.Application.Tests.Integration.Fixtures;
using CanBeYours.Application.UseCases.DemoThings.UpdateDemoThing;
using CanBeYours.Core.Domain.DemoThings;
using FluentAssertions;
using Microsoft.Extensions.Logging;

namespace CanBeYours.Application.Tests.Integration.UseCases.DemoThings;

/// <summary>
/// Integration tests for the UpdateDemoThing use case that verify the complete update flow with concurrency control.
/// This test class demonstrates how to test update operations that include entity retrieval, business rule validation,
/// concurrency-safe updates, and domain event publishing. The DemoThing functionality shown here is just an example
/// to help you learn how to implement your own unique features into the current codebase. You can use this testing
/// pattern to verify your actual update logic works correctly with real repositories and concurrency scenarios.
/// </summary>
[Collection(nameof(DemoThingsCollectionFixture))]
public class UpdateDemoThingTests
{
    private readonly DemoThingsCollectionFixture _fixture;
    private readonly UpdateDemoThingUseCase _updateDemoThingUseCase;

    /// <summary>
    /// Initializes the test class with the shared fixture and creates the use case instance.
    /// This constructor demonstrates how to set up test dependencies using the collection fixture
    /// to ensure proper test isolation and shared infrastructure.
    /// </summary>
    /// <param name="fixture">The shared fixture providing test infrastructure and dependencies</param>
    public UpdateDemoThingTests(DemoThingsCollectionFixture fixture)
    {
        _fixture = fixture;
        _updateDemoThingUseCase = GetUpdateDemoThingUseCase();
    }

    /// <summary>
    /// Tests that a DemoThing entity can be successfully updated with new data and persisted to the database.
    /// This test verifies the complete update flow including entity retrieval, business rule validation,
    /// concurrency-safe updates using version control, domain event publishing, and database persistence.
    /// It demonstrates how to test update logic in an integration environment with real data persistence.
    /// 
    /// Example test scenario: Updating an existing demo thing should modify the entity properties,
    /// increment the version, publish domain events, and persist changes to the database.
    /// </summary>
    [Fact]
    public async Task DemoThing_is_updated()
    {
        //Arrange
        var demoThing = DemoThing.Create("Test Name", "Test Description", DemoThingType.DemoType1, _fixture._currentUser.GetUserId());
        await _fixture.SeedDemoThingAsync(demoThing);
        var request = new UpdateDemoThingRequest(demoThing.Id, "Updated Name", "Updated Description", DemoThingType.DemoType2);

        //Act
        var result = await _updateDemoThingUseCase.Handle(request, CancellationToken.None);

        //Assert
        result.EntityId.Should().NotBeNull();
        var updatedDemoThing = await _fixture.GetDemoThingAsync(result.EntityId);
        updatedDemoThing.Name.Should().Be("Updated Name");
        updatedDemoThing.Description.Should().Be("Updated Description");
        updatedDemoThing.Type.Should().Be(DemoThingType.DemoType2);
    }

    /// <summary>
    /// Creates and configures the UpdateDemoThingUseCase instance with all required dependencies.
    /// This method demonstrates how to manually construct use case instances for testing purposes,
    /// including how to resolve services from the test fixture and set up all required dependencies
    /// such as repositories and request dispatchers for update operations.
    /// </summary>
    /// <returns>A configured UpdateDemoThingUseCase instance ready for testing</returns>
    private UpdateDemoThingUseCase GetUpdateDemoThingUseCase()
    {
        var logger = _fixture.GetRequiredService<ILogger<UpdateDemoThingUseCase>>();
        return new UpdateDemoThingUseCase(_fixture._demoThingRepository, _fixture._requestDispatcher, logger);
    }
}

Use this as a reference for use-case-level integration testing:

  1. Seed real data through the fixture.
  2. Execute the use case under test.
  3. Assert response values.
  4. Assert persisted state from repository/database.

When you implement your own feature, keep tests close to use-case behavior and verify end-to-end correctness across application and infrastructure boundaries.

The template pattern is a recommended baseline, not a requirement. You can use your own integration-testing strategy if it better fits your team.