Files
ColaFlow/colaflow-api/tests/IntegrationTestBase.cs
Yaojia Wang 014d62bcc2 Project Init
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-02 23:55:18 +01:00

198 lines
6.3 KiB
C#

using System;
using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Testcontainers.PostgreSql;
using Testcontainers.Redis;
using Xunit;
namespace ColaFlow.IntegrationTests.Infrastructure;
/// <summary>
/// Base class for integration tests that require PostgreSQL and Redis
/// Uses Testcontainers to spin up isolated database instances
/// </summary>
public abstract class IntegrationTestBase : IAsyncLifetime
{
// PostgreSQL Container
protected PostgreSqlContainer PostgresContainer { get; private set; } = null!;
// Redis Container
protected RedisContainer RedisContainer { get; private set; } = null!;
// Connection Strings
protected string PostgresConnectionString => PostgresContainer.GetConnectionString();
protected string RedisConnectionString => RedisContainer.GetConnectionString();
/// <summary>
/// Initialize containers before tests
/// Called by xUnit before any test in the class runs
/// </summary>
public virtual async Task InitializeAsync()
{
// Create PostgreSQL container
PostgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.WithDatabase("colaflow_test")
.WithUsername("colaflow_test")
.WithPassword("colaflow_test_password")
.WithCleanUp(true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432))
.Build();
// Create Redis container
RedisContainer = new RedisBuilder()
.WithImage("redis:7-alpine")
.WithCleanUp(true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
// Start containers in parallel
await Task.WhenAll(
PostgresContainer.StartAsync(),
RedisContainer.StartAsync()
);
// Optional: Run migrations or seed data
await SeedDatabaseAsync();
}
/// <summary>
/// Cleanup containers after tests
/// Called by xUnit after all tests in the class complete
/// </summary>
public virtual async Task DisposeAsync()
{
// Stop containers in parallel
await Task.WhenAll(
PostgresContainer.StopAsync(),
RedisContainer.StopAsync()
);
// Dispose containers
await PostgresContainer.DisposeAsync();
await RedisContainer.DisposeAsync();
}
/// <summary>
/// Seed database with test data
/// Override in derived classes for custom seeding
/// </summary>
protected virtual async Task SeedDatabaseAsync()
{
// Example: Create tables, seed data, etc.
await using var connection = new NpgsqlConnection(PostgresConnectionString);
await connection.OpenAsync();
// Create extensions
await using var command = connection.CreateCommand();
command.CommandText = @"
CREATE EXTENSION IF NOT EXISTS ""uuid-ossp"";
CREATE EXTENSION IF NOT EXISTS ""pg_trgm"";
";
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// Create DbContextOptions for Entity Framework Core
/// </summary>
protected DbContextOptions<TContext> CreateDbContextOptions<TContext>()
where TContext : DbContext
{
return new DbContextOptionsBuilder<TContext>()
.UseNpgsql(PostgresConnectionString)
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
.Options;
}
/// <summary>
/// Execute SQL command on test database
/// </summary>
protected async Task ExecuteSqlAsync(string sql)
{
await using var connection = new NpgsqlConnection(PostgresConnectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = sql;
await command.ExecuteNonQueryAsync();
}
/// <summary>
/// Clean database tables for test isolation
/// </summary>
protected async Task CleanDatabaseAsync()
{
await ExecuteSqlAsync(@"
DO $$
DECLARE
r RECORD;
BEGIN
-- Disable triggers
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END $$;
");
}
}
/// <summary>
/// Collection fixture for sharing Testcontainers across multiple test classes
/// Use [Collection("IntegrationTests")] attribute on test classes
/// </summary>
[CollectionDefinition("IntegrationTests")]
public class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture>
{
// This class has no code, and is never created.
// Its purpose is simply to be the place to apply [CollectionDefinition]
}
/// <summary>
/// Shared fixture for integration tests
/// Containers are created once and shared across test classes
/// </summary>
public class IntegrationTestFixture : IAsyncLifetime
{
public PostgreSqlContainer PostgresContainer { get; private set; } = null!;
public RedisContainer RedisContainer { get; private set; } = null!;
public string PostgresConnectionString => PostgresContainer.GetConnectionString();
public string RedisConnectionString => RedisContainer.GetConnectionString();
public async Task InitializeAsync()
{
// Create containers
PostgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.WithDatabase("colaflow_test")
.WithUsername("colaflow_test")
.WithPassword("colaflow_test_password")
.WithCleanUp(true)
.Build();
RedisContainer = new RedisBuilder()
.WithImage("redis:7-alpine")
.WithCleanUp(true)
.Build();
// Start containers
await Task.WhenAll(
PostgresContainer.StartAsync(),
RedisContainer.StartAsync()
);
}
public async Task DisposeAsync()
{
await Task.WhenAll(
PostgresContainer.DisposeAsync().AsTask(),
RedisContainer.DisposeAsync().AsTask()
);
}
}