Skip to content

Test Data Management in ConnectSoft Microservice Template

Purpose & Overview

Test Data Management is the systematic approach to creating, organizing, and maintaining test data for unit tests, integration tests, and acceptance tests. In the ConnectSoft Microservice Template, test data management follows patterns that ensure tests are isolated, maintainable, and reliable, providing consistent test data while avoiding test interdependencies.

Test data management provides:

  • Test Isolation: Each test has its own data, preventing interference between tests
  • Maintainability: Centralized test data creation reduces duplication
  • Readability: Clear, expressive test data setup improves test comprehension
  • Flexibility: Easy to create variations of test data for different scenarios
  • Performance: Efficient data setup and cleanup strategies
  • Reliability: Deterministic test data ensures consistent test results
  • Reusability: Shared builders and factories reduce code duplication

Test Data Management Philosophy

Test data management is about creating the right data for the right test at the right time. The template emphasizes using builders and factories to create test data expressively, isolating tests with fresh data for each test run, and cleaning up data after tests to prevent interference. Test data should be as simple as possible while still being realistic enough to catch real-world issues.

Architecture Overview

Test Data Management Flow

Test Execution
Test Setup (TestInitialize / Constructor)
    ├── Database Cleanup (if needed)
    ├── Test Data Creation
    │   ├── Builders (Fluent API)
    │   ├── Factories (Reusable Methods)
    │   └── Seeders (Bulk Data)
    └── Test Fixture Initialization
Test Execution
    ├── Use Test Data
    └── Create Additional Test Data (if needed)
Test Cleanup (TestCleanup / Dispose)
    ├── Database Cleanup
    ├── Resource Disposal
    └── State Reset

Test Data Management Components

Test Data Management
├── Builders
│   ├── Fluent API for Data Creation
│   ├── Default Values
│   └── Customization Methods
├── Factories
│   ├── Reusable Test Data Creation
│   ├── Scenario-Specific Data
│   └── Random Data Generation
├── Seeders
│   ├── Database Seeding
│   ├── Bulk Data Creation
│   └── Reference Data Setup
├── Fixtures
│   ├── Shared Test Setup
│   ├── Test Environment Configuration
│   └── Resource Management
└── Cleanup Strategies
    ├── Per-Test Cleanup
    ├── Per-Class Cleanup
    ├── Transaction Rollback
    └── Database Truncation

Test Data Creation Patterns

Builder Pattern

Purpose: Fluent API for creating test data with default values and customization options.

Example:

public class MicroserviceAggregateRootEntityBuilder
{
    private Guid objectId = Guid.NewGuid();
    private string name = "Test Aggregate Root";
    private DateTime createdAt = DateTime.UtcNow;

    public MicroserviceAggregateRootEntityBuilder WithObjectId(Guid id)
    {
        this.objectId = id;
        return this;
    }

    public MicroserviceAggregateRootEntityBuilder WithName(string name)
    {
        this.name = name;
        return this;
    }

    public MicroserviceAggregateRootEntityBuilder WithCreatedAt(DateTime createdAt)
    {
        this.createdAt = createdAt;
        return this;
    }

    public MicroserviceAggregateRootEntity Build()
    {
        return new MicroserviceAggregateRootEntity
        {
            ObjectId = this.objectId,
            Name = this.name,
            CreatedAt = this.createdAt
        };
    }

    // Convenience methods for common scenarios
    public static MicroserviceAggregateRootEntityBuilder Valid()
    {
        return new MicroserviceAggregateRootEntityBuilder();
    }

    public static MicroserviceAggregateRootEntityBuilder WithId(Guid id)
    {
        return new MicroserviceAggregateRootEntityBuilder().WithObjectId(id);
    }
}

Usage:

// Basic usage
var entity = new MicroserviceAggregateRootEntityBuilder()
    .WithName("My Test Entity")
    .WithObjectId(Guid.Parse("11111111-1111-1111-1111-111111111111"))
    .Build();

// Using convenience methods
var entity = MicroserviceAggregateRootEntityBuilder.Valid()
    .WithName("Custom Name")
    .Build();

// Default values (minimal setup)
var entity = new MicroserviceAggregateRootEntityBuilder().Build();

Benefits: - Fluent API: Readable and expressive - Default Values: Sensible defaults reduce boilerplate - Flexibility: Easy to customize for specific test scenarios - Reusability: Shared across multiple tests

Factory Pattern

Purpose: Reusable methods for creating test data with predefined scenarios.

Example:

public static class TestDataFactory
{
    public static MicroserviceAggregateRootEntity CreateValidEntity()
    {
        return new MicroserviceAggregateRootEntity
        {
            ObjectId = Guid.NewGuid(),
            Name = "Test Entity",
            CreatedAt = DateTime.UtcNow
        };
    }

    public static MicroserviceAggregateRootEntity CreateEntityWithId(Guid id)
    {
        return new MicroserviceAggregateRootEntity
        {
            ObjectId = id,
            Name = $"Entity {id}",
            CreatedAt = DateTime.UtcNow
        };
    }

    public static CreateMicroserviceAggregateRootInput CreateValidInput()
    {
        return new CreateMicroserviceAggregateRootInput
        {
            ObjectId = Guid.NewGuid()
        };
    }

    public static CreateMicroserviceAggregateRootRequest CreateValidRequest()
    {
        return new CreateMicroserviceAggregateRootRequest
        {
            ObjectId = Guid.NewGuid()
        };
    }

    // Scenario-specific factories
    public static MicroserviceAggregateRootEntity CreateDeletedEntity()
    {
        return new MicroserviceAggregateRootEntity
        {
            ObjectId = Guid.NewGuid(),
            Name = "Deleted Entity",
            CreatedAt = DateTime.UtcNow.AddDays(-1),
            DeletedAt = DateTime.UtcNow
        };
    }
}

Usage:

// Simple factory method
var entity = TestDataFactory.CreateValidEntity();

// Scenario-specific factory
var deletedEntity = TestDataFactory.CreateDeletedEntity();

// Factory with parameters
var entity = TestDataFactory.CreateEntityWithId(testId);

Benefits: - Simplicity: Quick creation of common scenarios - Consistency: Same data structure across tests - Scenario-Specific: Predefined scenarios for common cases

Seeder Pattern

Purpose: Bulk data creation for integration tests and database seeding.

Example:

public static class DatabaseSeeder
{
    public static async Task SeedTestDataAsync(
        IMicroserviceAggregateRootsRepository repository)
    {
        var entities = new[]
        {
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.Parse("11111111-1111-1111-1111-111111111111"),
                Name = "Test Entity 1",
                CreatedAt = DateTime.UtcNow
            },
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.Parse("22222222-2222-2222-2222-222222222222"),
                Name = "Test Entity 2",
                CreatedAt = DateTime.UtcNow
            },
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.Parse("33333333-3333-3333-3333-333333333333"),
                Name = "Test Entity 3",
                CreatedAt = DateTime.UtcNow
            }
        };

        foreach (var entity in entities)
        {
            await repository.InsertAsync(entity);
        }
    }

    public static async Task SeedReferenceDataAsync(
        IMicroserviceAggregateRootsRepository repository)
    {
        // Seed reference data (lookup tables, etc.)
        var referenceEntities = new[]
        {
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.Parse("AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"),
                Name = "Reference Entity 1"
            }
        };

        foreach (var entity in referenceEntities)
        {
            await repository.InsertAsync(entity);
        }
    }
}

Usage:

[TestInitialize]
public async Task Setup()
{
    using var scope = this.server.Services.CreateScope();
    var repository = scope.ServiceProvider
        .GetRequiredService<IMicroserviceAggregateRootsRepository>();

    await DatabaseSeeder.SeedTestDataAsync(repository);
}

Benefits: - Bulk Operations: Efficient for seeding multiple entities - Reference Data: Setup lookup tables and reference data - Consistency: Same seed data across multiple tests

Database Seeding

In-Memory Database Seeding

NHibernate In-Memory:

[TestClass]
public class RepositoryIntegrationTests
{
    private ISessionFactory? sessionFactory;
    private ISession? session;

    [TestInitialize]
    public void Setup()
    {
        // Configure in-memory SQLite database
        var configuration = new Configuration()
            .Configure()
            .SetProperty(NHibernate.Cfg.Environment.ConnectionString,
                "Data Source=:memory:;Version=3;New=True;")
            .SetProperty(NHibernate.Cfg.Environment.Dialect,
                typeof(SQLiteDialect).AssemblyQualifiedName);

        // Add mappings
        var modelMapper = new ModelMapper();
        modelMapper.AddMapping<MicroserviceAggregateRootEntityMap>();
        configuration.AddMapping(modelMapper.CompileMappingForAllExplicitlyAddedEntities());

        this.sessionFactory = configuration.BuildSessionFactory();
        this.session = this.sessionFactory.OpenSession();

        // Create schema
        var schemaExport = new SchemaExport(configuration);
        schemaExport.Execute(true, true, false, this.session.Connection, null);

        // Seed test data
        this.SeedTestData();
    }

    private void SeedTestData()
    {
        var repository = new MicroserviceAggregateRootsRepository(this.session!);
        var entities = new[]
        {
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.NewGuid(),
                Name = "Test Entity 1"
            },
            new MicroserviceAggregateRootEntity
            {
                ObjectId = Guid.NewGuid(),
                Name = "Test Entity 2"
            }
        };

        foreach (var entity in entities)
        {
            repository.Insert(entity);
        }

        this.session!.Flush();
    }

    [TestCleanup]
    public void Cleanup()
    {
        this.session?.Dispose();
        this.sessionFactory?.Dispose();
    }
}

Test Container Database Seeding

Using Testcontainers:

[TestClass]
public class DatabaseIntegrationTests : IAsyncLifetime
{
    private SqlServerContainer? sqlServerContainer;
    private IServiceProvider? serviceProvider;

    public async Task InitializeAsync()
    {
        // Start SQL Server container
        this.sqlServerContainer = new SqlServerBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPassword("Test123!")
            .Build();

        await this.sqlServerContainer.StartAsync();

        // Configure services with container connection string
        var services = new ServiceCollection();
        var connectionString = this.sqlServerContainer.GetConnectionString();

        // Configure NHibernate with container connection string
        services.AddNHibernatePersistenceModel(connectionString);

        this.serviceProvider = services.BuildServiceProvider();

        // Run migrations
        var migrationRunner = this.serviceProvider
            .GetRequiredService<MigrationRunner>();
        migrationRunner.Up();

        // Seed test data
        await this.SeedTestDataAsync();
    }

    private async Task SeedTestDataAsync()
    {
        using var scope = this.serviceProvider!.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        await DatabaseSeeder.SeedTestDataAsync(repository);
    }

    public async Task DisposeAsync()
    {
        await this.sqlServerContainer?.StopAsync()!;
    }
}

MongoDB Seeding

MongoDB Test Database:

[TestClass]
public class MongoDbIntegrationTests
{
    private MongoClient? mongoClient;
    private IServiceProvider? serviceProvider;
    private const string DbName = "TestDatabase";

    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.Test.json")
            .Build();

        var services = new ServiceCollection();
        services.AddLogging();

        mongoClient = new MongoClient(
            configuration.GetConnectionString("MongoDb"));

        // Drop database if exists
        var foundName = mongoClient
            .ListDatabaseNames()
            .ToList()
            .FirstOrDefault(s => string.Equals(s, DbName, StringComparison.Ordinal));

        if (!string.IsNullOrEmpty(foundName))
        {
            mongoClient.DropDatabase(DbName);
        }

        services.UseMongoDbPersistence(configuration, DbName, DbName);
        services.AddScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsRepository>();

        serviceProvider = services.BuildServiceProvider();

        // Run migrations
        var migrationRunner = serviceProvider
            .GetRequiredService<MigrationRunner>();
        migrationRunner.Up();
    }

    [TestInitialize]
    public async Task Setup()
    {
        // Seed test data for each test
        using var scope = serviceProvider!.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        await DatabaseSeeder.SeedTestDataAsync(repository);
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        serviceProvider?.GetRequiredService<MigrationRunner>().Up(version: 0);
        mongoClient?.DropDatabase(DbName);
    }
}

Test Isolation

Per-Test Isolation

Fresh Data for Each Test:

[TestClass]
public class ControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    [TestInitialize]
    public async Task Setup()
    {
        // Clear database before each test
        await this.ClearDatabaseAsync();

        // Seed fresh test data
        await this.SeedTestDataAsync();
    }

    [TestCleanup]
    public async Task Cleanup()
    {
        // Clean up test-specific data
        await this.CleanupTestDataAsync();
    }

    private async Task ClearDatabaseAsync()
    {
        var client = this.factory.CreateClient();
        var server = this.factory.Server;

        using var scope = server.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        // Clear all test data
        var allEntities = await repository.GetAllAsync();
        foreach (var entity in allEntities)
        {
            await repository.DeleteAsync(entity.ObjectId);
        }
    }

    private async Task SeedTestDataAsync()
    {
        var server = this.factory.Server;
        using var scope = server.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        await DatabaseSeeder.SeedTestDataAsync(repository);
    }

    private async Task CleanupTestDataAsync()
    {
        // Clean up any test-specific data created during test
    }
}

Transaction-Based Isolation

Rollback Transactions After Each Test:

[TestClass]
public class RepositoryTests
{
    private ISession? session;
    private ITransaction? transaction;

    [TestInitialize]
    public void Setup()
    {
        this.session = this.sessionFactory.OpenSession();
        this.transaction = this.session.BeginTransaction();

        // Seed test data within transaction
        this.SeedTestData();
    }

    [TestCleanup]
    public void Cleanup()
    {
        // Rollback transaction to undo all changes
        this.transaction?.Rollback();
        this.transaction?.Dispose();
        this.session?.Dispose();
    }

    [TestMethod]
    public async Task Repository_Should_Save_Entity()
    {
        // Test within transaction
        var repository = new MicroserviceAggregateRootsRepository(this.session!);
        var entity = TestDataFactory.CreateValidEntity();

        await repository.InsertAsync(entity);
        await this.session!.FlushAsync();

        // Assertions
        var retrieved = await repository.GetByIdAsync(entity.ObjectId);
        Assert.IsNotNull(retrieved);

        // Transaction will be rolled back in cleanup
    }
}

Unique Database Per Test

Isolated Database Instances:

[TestClass]
public class IsolatedDatabaseTests
{
    private string testDatabaseName;

    [TestInitialize]
    public void Setup()
    {
        // Create unique database name for this test
        this.testDatabaseName = $"TestDb_{Guid.NewGuid():N}";

        // Create and configure database
        this.CreateTestDatabase(this.testDatabaseName);
        this.SeedTestData();
    }

    [TestCleanup]
    public void Cleanup()
    {
        // Drop test database
        this.DropTestDatabase(this.testDatabaseName);
    }

    private void CreateTestDatabase(string dbName)
    {
        // Create database with unique name
        // Configure NHibernate/MongoDB with this database
    }

    private void DropTestDatabase(string dbName)
    {
        // Drop the test database
    }
}

Test Fixtures

Base Test Fixture

Shared Setup and Cleanup:

public abstract class IntegrationTestBase
{
    protected WebApplicationFactory<Program> Factory { get; private set; }
    protected HttpClient Client { get; private set; }
    protected IServiceProvider Services { get; private set; }
    protected string TestTenantId { get; } = "test-tenant-001";

    [TestInitialize]
    public virtual void TestInitialize()
    {
        this.Factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureServices(services =>
                {
                    // Override services for testing
                });
            });

        this.Client = this.Factory.CreateClient();
        this.Services = this.Factory.Services;

        // Set default test headers
        this.Client.DefaultRequestHeaders.Add("X-Test-Tenant-Id", this.TestTenantId);
        this.Client.DefaultRequestHeaders.Add("X-Test-User-Id", "test-user");

        // Seed test data
        this.SeedTestData();
    }

    [TestCleanup]
    public virtual void TestCleanup()
    {
        // Clean up test data
        this.CleanupTestData();

        this.Client?.Dispose();
        this.Factory?.Dispose();
    }

    protected abstract void SeedTestData();
    protected abstract void CleanupTestData();
}

// Usage
[TestClass]
public class ControllerTests : IntegrationTestBase
{
    protected override void SeedTestData()
    {
        using var scope = this.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        // Seed test data
        var entity = TestDataFactory.CreateValidEntity();
        repository.Insert(entity);
    }

    protected override void CleanupTestData()
    {
        using var scope = this.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        // Clean up test data
        var allEntities = repository.GetAll();
        foreach (var entity in allEntities)
        {
            repository.Delete(entity.ObjectId);
        }
    }

    [TestMethod]
    public async Task Get_Should_Return_Ok()
    {
        var response = await this.Client.GetAsync("/api/aggregateroots");
        response.EnsureSuccessStatusCode();
    }
}

Processor Test Fixture

Domain-Specific Test Fixtures:

public class ProcessorTestFixture
{
    public Mock<IMicroserviceAggregateRootsRepository> MockRepository { get; }
    public Mock<IUnitOfWork> MockUnitOfWork { get; }
    public Mock<IEventBus> MockEventBus { get; }
    public DefaultMicroserviceAggregateRootsProcessor Processor { get; }

    public ProcessorTestFixture()
    {
        this.MockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
        this.MockUnitOfWork = new Mock<IUnitOfWork>();
        this.MockEventBus = new Mock<IEventBus>();

        this.Processor = new DefaultMicroserviceAggregateRootsProcessor(
            Mock.Of<ILogger<DefaultMicroserviceAggregateRootsProcessor>>(),
            TimeProvider.System,
            this.MockRepository.Object,
            this.MockUnitOfWork.Object,
            this.MockEventBus.Object,
            Mock.Of<IValidator<CreateMicroserviceAggregateRootInput>>(),
            Mock.Of<IValidator<DeleteMicroserviceAggregateRootInput>>(),
            Mock.Of<MicroserviceTemplateMetrics>());
    }

    public void SetupEntityExists(Guid id)
    {
        this.MockRepository.Setup(r => r.GetByIdAsync(id))
            .ReturnsAsync(new MicroserviceAggregateRootEntity 
            { 
                ObjectId = id 
            });
    }

    public void SetupEntityNotFound(Guid id)
    {
        this.MockRepository.Setup(r => r.GetByIdAsync(id))
            .ReturnsAsync((IMicroserviceAggregateRoot?)null);
    }
}

// Usage
[TestClass]
public class ProcessorTests
{
    private ProcessorTestFixture fixture;

    [TestInitialize]
    public void Setup()
    {
        this.fixture = new ProcessorTestFixture();
    }

    [TestMethod]
    public async Task Create_WithValidInput_Should_Succeed()
    {
        // Arrange
        var input = TestDataFactory.CreateValidInput();
        this.fixture.SetupEntityNotFound(input.ObjectId);

        // Act
        var result = await this.fixture.Processor
            .CreateMicroserviceAggregateRoot(input);

        // Assert
        Assert.IsNotNull(result);
        this.fixture.MockRepository.Verify(
            r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()), 
            Times.Once);
    }
}

Cleanup Strategies

Database Cleanup in TestStartup

Cleanup Before Test Suite:

public class TestStartup : Startup
{
    public override void ConfigureServices(IServiceCollection services)
    {
#if UseNHibernate
        // Drop database if exists before starting tests
        this.DropSqlDatabase(TestConstants.DbName);
#endif
#if UseMongoDb
        // Drop MongoDB database if exists
        this.DropMongoDbDatabase(TestConstants.DbName);
#endif

        base.ConfigureServices(services);
    }

#if UseNHibernate
    private void DropSqlDatabase(string dbName)
    {
        var connectionString = this.Configuration
            .GetConnectionString(TestConstants.NHibernateConnectionStringKey);

        if (!IsDatabaseExists(connectionString, dbName))
        {
            return;
        }

        new SqlServerDatabaseHelper().ExecuteSql(
            connectionString,
            $"Use master;ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;DROP DATABASE [{dbName}];");
    }
#endif

#if UseMongoDb
    private void DropMongoDbDatabase(string dbName)
    {
        var mongoClient = new MongoClient(
            this.Configuration.GetConnectionString(TestConstants.MongoDbConnectionStringKey));

        var foundName = mongoClient.ListDatabaseNames()
            .ToList()
            .FirstOrDefault(s => string.Equals(s, dbName, StringComparison.Ordinal));

        if (!string.IsNullOrEmpty(foundName))
        {
            mongoClient.DropDatabase(dbName);
        }
    }
#endif
}

Per-Class Cleanup

Cleanup After All Tests in Class:

[TestClass]
public class DatabaseTests
{
    private static IServiceProvider? serviceProvider;
    private static MongoClient? mongoClient;
    private const string DbName = "TestDatabase";

    [ClassInitialize]
    public static void ClassInitialize(TestContext context)
    {
        // Setup database and services
        // ...
        serviceProvider = services.BuildServiceProvider();
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        // Clean up database after all tests
        serviceProvider?.GetRequiredService<MigrationRunner>().Up(version: 0);
        mongoClient?.DropDatabase(DbName);
    }

    [TestInitialize]
    public async Task Setup()
    {
        // Seed test data for each test
    }

    [TestCleanup]
    public async Task Cleanup()
    {
        // Clean up test-specific data
    }
}

Best Practices

Do's

  1. Use Builders for Complex Test Data

    // ✅ GOOD - Fluent, readable
    var entity = new MicroserviceAggregateRootEntityBuilder()
        .WithName("Test Entity")
        .WithObjectId(testId)
        .Build();
    

  2. Provide Sensible Defaults

    // ✅ GOOD - Default values reduce boilerplate
    public class EntityBuilder
    {
        private Guid objectId = Guid.NewGuid(); // Sensible default
        private string name = "Test Entity"; // Sensible default
    }
    

  3. Isolate Tests with Fresh Data

    // ✅ GOOD - Fresh data for each test
    [TestInitialize]
    public async Task Setup()
    {
        await this.ClearDatabaseAsync();
        await this.SeedTestDataAsync();
    }
    

  4. Use Factories for Common Scenarios

    // ✅ GOOD - Reusable factory methods
    var entity = TestDataFactory.CreateValidEntity();
    var deletedEntity = TestDataFactory.CreateDeletedEntity();
    

  5. Clean Up After Tests

    // ✅ GOOD - Proper cleanup
    [TestCleanup]
    public async Task Cleanup()
    {
        await this.CleanupTestDataAsync();
    }
    

  6. Use Unique Identifiers

    // ✅ GOOD - Unique IDs prevent conflicts
    var entity = new MicroserviceAggregateRootEntityBuilder()
        .WithObjectId(Guid.NewGuid())
        .Build();
    

Don'ts

  1. Don't Share State Between Tests

    // ❌ BAD - Shared state causes test interference
    private static List<Entity> sharedEntities = new();
    
    // ✅ GOOD - Each test has isolated state
    private List<Entity> testEntities = new();
    

  2. Don't Hardcode Test Data

    // ❌ BAD - Hardcoded values
    var id = Guid.Parse("12345678-1234-1234-1234-123456789012");
    
    // ✅ GOOD - Generated or builder-based
    var id = Guid.NewGuid();
    var entity = new EntityBuilder().WithObjectId(id).Build();
    

  3. Don't Skip Cleanup

    // ❌ BAD - No cleanup, data persists
    [TestMethod]
    public async Task Test()
    {
        // Test code
    }
    
    // ✅ GOOD - Proper cleanup
    [TestCleanup]
    public async Task Cleanup()
    {
        await this.CleanupTestDataAsync();
    }
    

  4. Don't Use Production Data

    // ❌ BAD - Production data in tests
    var entity = await repository.GetByIdAsync(productionId);
    
    // ✅ GOOD - Test-specific data
    var entity = TestDataFactory.CreateValidEntity();
    

  5. Don't Create Unnecessary Dependencies

    // ❌ BAD - Creates unnecessary related entities
    var entity = new EntityBuilder()
        .WithRelatedEntities(10) // Too many dependencies
        .Build();
    
    // ✅ GOOD - Minimal required data
    var entity = new EntityBuilder()
        .WithRequiredFields()
        .Build();
    

  6. Don't Ignore Test Isolation

    // ❌ BAD - Tests depend on execution order
    [TestMethod]
    public void Test1() { /* Creates entity with ID 1 */ }
    [TestMethod]
    public void Test2() { /* Expects entity with ID 1 */ }
    
    // ✅ GOOD - Each test is independent
    [TestMethod]
    public void Test1() 
    { 
        var entity = TestDataFactory.CreateEntityWithId(id1);
    }
    [TestMethod]
    public void Test2() 
    { 
        var entity = TestDataFactory.CreateEntityWithId(id2);
    }
    

Common Scenarios

Scenario 1: Unit Test with Mock Data

Setup:

[TestMethod]
public async Task Processor_WithValidInput_Should_Succeed()
{
    // Arrange
    var mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
    var input = TestDataFactory.CreateValidInput();

    mockRepository.Setup(r => r.GetByIdAsync(input.ObjectId))
        .ReturnsAsync((IMicroserviceAggregateRoot?)null);

    var processor = new DefaultMicroserviceAggregateRootsProcessor(
        /* dependencies */);

    // Act
    var result = await processor.CreateMicroserviceAggregateRoot(input);

    // Assert
    Assert.IsNotNull(result);
}

Scenario 2: Integration Test with Database

Setup:

[TestClass]
public class RepositoryIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> factory;

    [TestInitialize]
    public async Task Setup()
    {
        var server = this.factory.Server;
        using var scope = server.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        // Clear and seed test data
        await this.ClearDatabaseAsync(repository);
        await DatabaseSeeder.SeedTestDataAsync(repository);
    }

    [TestMethod]
    public async Task Get_WithSeededData_Should_Return_Entities()
    {
        var client = this.factory.CreateClient();
        var response = await client.GetAsync("/api/aggregateroots");

        response.EnsureSuccessStatusCode();
        var entities = await response.Content.ReadFromJsonAsync<List<EntityDto>>();

        Assert.IsTrue(entities!.Count > 0);
    }
}

Scenario 3: BDD Test with Scenario Data

Setup:

[Binding]
public class FeatureSteps
{
    private ScenarioContext scenarioContext;
    private WebApplicationFactory<Program> factory;

    [Given(@"I have a valid aggregate root")]
    public void GivenIHaveAValidAggregateRoot()
    {
        var entity = TestDataFactory.CreateValidEntity();
        this.scenarioContext["Entity"] = entity;
    }

    [Given(@"I have seeded test data")]
    public async Task GivenIHaveSeededTestData()
    {
        var server = this.factory.Server;
        using var scope = server.Services.CreateScope();
        var repository = scope.ServiceProvider
            .GetRequiredService<IMicroserviceAggregateRootsRepository>();

        await DatabaseSeeder.SeedTestDataAsync(repository);
    }
}

Troubleshooting

Issue: Tests Interfere with Each Other

Symptoms: Tests pass individually but fail when run together.

Solutions: 1. Ensure Test Isolation:

[TestInitialize]
public async Task Setup()
{
    await this.ClearDatabaseAsync();
    await this.SeedTestDataAsync();
}

  1. Use Unique Identifiers:

    var entity = new EntityBuilder()
        .WithObjectId(Guid.NewGuid()) // Unique for each test
        .Build();
    

  2. Use Transactions with Rollback:

    [TestCleanup]
    public void Cleanup()
    {
        this.transaction?.Rollback(); // Undo all changes
    }
    

Issue: Database State Persists Between Test Runs

Symptoms: Old test data appears in new test runs.

Solutions: 1. Clean Database in TestStartup:

public override void ConfigureServices(IServiceCollection services)
{
    this.DropSqlDatabase(TestConstants.DbName);
    base.ConfigureServices(services);
}

  1. Use ClassCleanup:
    [ClassCleanup]
    public static void ClassCleanup()
    {
        mongoClient?.DropDatabase(DbName);
    }
    

Issue: Test Data Setup is Slow

Symptoms: Tests take too long to run.

Solutions: 1. Use In-Memory Databases:

// Fast in-memory database
var connectionString = "Data Source=:memory:;Version=3;New=True;";

  1. Seed Only Required Data:

    // ✅ GOOD - Seed only what's needed
    var entity = TestDataFactory.CreateValidEntity();
    
    // ❌ BAD - Seed unnecessary data
    await DatabaseSeeder.SeedTestDataAsync(); // Seeds 100 entities
    

  2. Use Test Fixtures for Shared Setup:

    // Shared factory across tests
    public class ControllerTests : IClassFixture<WebApplicationFactory<Program>>
    {
        // Factory created once, reused for all tests
    }
    

Issue: Test Data Not Found

Symptoms: Tests fail with "entity not found" errors.

Solutions: 1. Verify Seeding Completed:

await DatabaseSeeder.SeedTestDataAsync(repository);
await repository.FlushAsync(); // Ensure data is persisted

  1. Check Transaction Scope:

    // Ensure same transaction/scope
    using var scope = server.Services.CreateScope();
    var repository = scope.ServiceProvider.GetRequiredService<IRepository>();
    

  2. Use Explicit IDs:

    var entity = new EntityBuilder()
        .WithObjectId(Guid.Parse("11111111-1111-1111-1111-111111111111"))
        .Build();
    

Summary

Test data management in the ConnectSoft Microservice Template provides:

  • Builder Pattern: Fluent API for creating test data
  • Factory Pattern: Reusable test data creation methods
  • Seeder Pattern: Bulk data seeding for integration tests
  • Test Isolation: Fresh data for each test
  • Cleanup Strategies: Proper resource cleanup
  • Test Fixtures: Shared setup and configuration
  • Best Practices: Patterns for maintainable test data

By following these patterns, teams can:

  • Create Reliable Tests: Isolated tests that don't interfere with each other
  • Improve Maintainability: Centralized test data creation reduces duplication
  • Enhance Readability: Clear, expressive test data setup
  • Ensure Performance: Efficient data setup and cleanup
  • Enable Reusability: Shared builders and factories across tests

Test data management is essential for creating robust, maintainable test suites that provide confidence in code correctness while remaining fast and reliable.