dotnet-testing-advanced-testcontainers-database

使用 Testcontainers 進行容器化資料庫測試的專門技能。當需要測試真實資料庫行為、使用 SQL Server/PostgreSQL/MySQL 容器、測試 EF Core/Dapper 時使用。涵蓋容器啟動、資料庫遷移、測試隔離、容器共享等。 Make sure to use this skill whenever the user mentions Testcontainers database, SQL Server container, PostgreSQL container, EF Core integration test, Dapper testing, or Collection Fixture, even if they don't explicitly ask for container-based database testing. Keywords: testcontainers, 容器測試, container testing, database testing, 資料庫測試, MsSqlContainer, PostgreSqlContainer, MySqlContainer, EF Core testing, Dapper testing, Testcontainers.MsSql, Testcontainers.PostgreSql, GetConnectionString, IAsyncLifetime, CollectionFixture

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-testing-advanced-testcontainers-database" with this command: npx skills add kevintsengtw/dotnet-testing-agent-skills/kevintsengtw-dotnet-testing-agent-skills-dotnet-testing-advanced-testcontainers-database

Testcontainers 資料庫整合測試指南

EF Core InMemory 的限制

在選擇測試策略前,必須了解 EF Core InMemory 資料庫的重大限制:

1. 交易行為與資料庫鎖定

  • 不支援資料庫交易 (Transactions)SaveChanges() 後資料立即儲存,無法進行 Rollback
  • 無資料庫鎖定機制:無法模擬並發 (Concurrency) 情境下的行為

2. LINQ 查詢差異

  • 查詢翻譯差異:某些 LINQ 查詢(複雜 GroupBy、JOIN、自訂函數)在 InMemory 中可執行,但轉換成 SQL 時可能失敗
  • Case Sensitivity:InMemory 預設不區分大小寫,但真實資料庫依賴校對規則 (Collation)
  • 效能模擬不足:無法模擬真實資料庫的效能瓶頸或索引問題

3. 資料庫特定功能

InMemory 模式無法測試:

  • 預存程序 (Stored Procedures) 與 Triggers
  • Views
  • 外鍵約束 (Foreign Key Constraints)、檢查約束 (Check Constraints)
  • 資料類型精確度(decimal、datetime 等)
  • Concurrency Tokens(RowVersion、Timestamp)

結論:當需要驗證複雜交易邏輯、並發處理、資料庫特定行為時,應使用 Testcontainers 進行整合測試。

Testcontainers 核心概念

什麼是 Testcontainers?

Testcontainers 是一個測試函式庫,提供輕量好用的 API 來啟動 Docker 容器,專門用於整合測試。

核心優勢

  1. 真實環境測試:使用真實資料庫,測試實際 SQL 語法與資料庫限制
  2. 環境一致性:確保測試環境與正式環境使用相同服務版本
  3. 清潔測試環境:每個測試有獨立乾淨的環境,容器自動清理
  4. 簡化開發環境:開發者只需 Docker,不需安裝各種服務

必要套件

<ItemGroup>
  <!-- 測試框架 -->
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
  <PackageReference Include="xunit" Version="2.9.3" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.9.3" />
  <PackageReference Include="AwesomeAssertions" Version="9.1.0" />

  <!-- Testcontainers 核心套件 -->
  <PackageReference Include="Testcontainers" Version="3.10.0" />

  <!-- 資料庫容器 -->
  <PackageReference Include="Testcontainers.PostgreSql" Version="3.10.0" />
  <PackageReference Include="Testcontainers.MsSql" Version="3.10.0" />

  <!-- Entity Framework Core -->
  <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
  <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />

  <!-- Dapper (可選) -->
  <PackageReference Include="Dapper" Version="2.1.35" />
  <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>

重要:使用 Microsoft.Data.SqlClient 而非舊版 System.Data.SqlClient,提供更好的效能與安全性。

環境需求

Docker Desktop 設定

  • Windows 10 版本 2004 或更新版本
  • 啟用 WSL 2 功能
  • 8GB RAM(建議 16GB 以上)
  • 64GB 可用磁碟空間

建議的 Docker Desktop Resources 設定

  • Memory: 6GB(系統記憶體的 50-75%)
  • CPUs: 4 cores
  • Swap: 2GB
  • Disk image size: 64GB

基本容器操作模式

PostgreSQL 容器

public class PostgreSqlTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres;
    private UserDbContext _dbContext = null!;

    public PostgreSqlTests()
    {
        _postgres = new PostgreSqlBuilder()
            .WithImage("postgres:15-alpine")
            .WithDatabase("testdb")
            .WithUsername("testuser")
            .WithPassword("testpass")
            .WithPortBinding(5432, true)  // 自動分配主機埠號
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _postgres.StartAsync();

        var options = new DbContextOptionsBuilder<UserDbContext>()
            .UseNpgsql(_postgres.GetConnectionString())
            .Options;

        _dbContext = new UserDbContext(options);
        await _dbContext.Database.EnsureCreatedAsync();
    }

    public async Task DisposeAsync()
    {
        await _dbContext.DisposeAsync();
        await _postgres.DisposeAsync();
    }
}

SQL Server 容器

public class SqlServerTests : IAsyncLifetime
{
    private readonly MsSqlContainer _container;
    private UserDbContext _dbContext = null!;

    public SqlServerTests()
    {
        _container = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPassword("YourStrong@Passw0rd")
            .WithCleanUp(true)
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _container.StartAsync();

        var options = new DbContextOptionsBuilder<UserDbContext>()
            .UseSqlServer(_container.GetConnectionString())
            .Options;

        _dbContext = new UserDbContext(options);
        await _dbContext.Database.EnsureCreatedAsync();
    }

    public async Task DisposeAsync()
    {
        await _dbContext.DisposeAsync();
        await _container.DisposeAsync();
    }
}

Collection Fixture 模式:容器共享

為什麼需要容器共享?

在大型專案中,每個測試類別都建立新容器會遇到嚴重的效能瓶頸:

  • 傳統方式:每個測試類別啟動一個容器。若有 3 個測試類別,總耗時約 3 × 10 秒 = 30 秒
  • Collection Fixture:所有測試類別共享同一個容器。總耗時僅約 1 × 10 秒 = 10 秒

測試執行時間減少約 67%

Collection Fixture 實作

/// <summary>
/// MSSQL 容器的 Collection Fixture
/// </summary>
public class SqlServerContainerFixture : IAsyncLifetime
{
    private readonly MsSqlContainer _container;

    public SqlServerContainerFixture()
    {
        _container = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-latest")
            .WithPassword("Test123456!")
            .WithCleanUp(true)
            .Build();
    }

    public static string ConnectionString { get; private set; } = string.Empty;

    public async Task InitializeAsync()
    {
        await _container.StartAsync();
        ConnectionString = _container.GetConnectionString();

        // 等待容器完全啟動
        await Task.Delay(2000);
    }

    public async Task DisposeAsync()
    {
        await _container.DisposeAsync();
    }
}

/// <summary>
/// 定義測試集合
/// </summary>
[CollectionDefinition(nameof(SqlServerCollectionFixture))]
public class SqlServerCollectionFixture : ICollectionFixture<SqlServerContainerFixture>
{
    // 此類別只是用來定義 Collection,不需要實作內容
}

測試類別整合

[Collection(nameof(SqlServerCollectionFixture))]
public class EfCoreTests : IDisposable
{
    private readonly ECommerceDbContext _dbContext;

    public EfCoreTests(ITestOutputHelper testOutputHelper)
    {
        var connectionString = SqlServerContainerFixture.ConnectionString;

        var options = new DbContextOptionsBuilder<ECommerceDbContext>()
            .UseSqlServer(connectionString)
            .EnableSensitiveDataLogging()
            .LogTo(testOutputHelper.WriteLine, LogLevel.Information)
            .Options;

        _dbContext = new ECommerceDbContext(options);
        _dbContext.Database.EnsureCreated();
    }

    public void Dispose()
    {
        // 按照外鍵約束順序清理資料
        _dbContext.Database.ExecuteSqlRaw("DELETE FROM OrderItems");
        _dbContext.Database.ExecuteSqlRaw("DELETE FROM Orders");
        _dbContext.Database.ExecuteSqlRaw("DELETE FROM Products");
        _dbContext.Database.ExecuteSqlRaw("DELETE FROM Categories");
        _dbContext.Dispose();
    }
}

SQL 腳本外部化策略

為什麼需要外部化 SQL 腳本?

  • 關注點分離:C# 程式碼專注於測試邏輯,SQL 腳本專注於資料庫結構
  • 可維護性:修改資料庫結構時,只需編輯 .sql 檔案
  • 可讀性:C# 程式碼變得更簡潔
  • 工具支援:SQL 檔案可獲得編輯器的語法高亮和格式化支援
  • 版本控制友善:SQL 變更可在版本控制系統中清楚追蹤

資料夾結構

tests/DatabaseTesting.Tests/
├── SqlScripts/
│   ├── Tables/
│   │   ├── CreateCategoriesTable.sql
│   │   ├── CreateProductsTable.sql
│   │   ├── CreateOrdersTable.sql
│   │   └── CreateOrderItemsTable.sql
│   └── StoredProcedures/
│       └── GetProductSalesReport.sql

.csproj 設定

<ItemGroup>
  <Content Include="SqlScripts\**\*.sql">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>
</ItemGroup>

腳本載入實作

private void EnsureTablesExist()
{
    var scriptDirectory = Path.Combine(AppContext.BaseDirectory, "SqlScripts");
    if (!Directory.Exists(scriptDirectory)) return;

    // 按照依賴順序執行表格建立腳本
    var orderedScripts = new[]
    {
        "Tables/CreateCategoriesTable.sql",
        "Tables/CreateProductsTable.sql",
        "Tables/CreateOrdersTable.sql",
        "Tables/CreateOrderItemsTable.sql"
    };

    foreach (var scriptPath in orderedScripts)
    {
        var fullPath = Path.Combine(scriptDirectory, scriptPath);
        if (File.Exists(fullPath))
        {
            var script = File.ReadAllText(fullPath);
            _dbContext.Database.ExecuteSqlRaw(script);
        }
    }
}

Wait Strategy 最佳實務

內建 Wait Strategy

// 等待特定埠號可用
var postgres = new PostgreSqlBuilder()
    .WithWaitStrategy(Wait.ForUnixContainer()
        .UntilPortIsAvailable(5432))
    .Build();

// 等待日誌訊息出現
var sqlServer = new MsSqlBuilder()
    .WithWaitStrategy(Wait.ForUnixContainer()
        .UntilPortIsAvailable(1433)
        .UntilMessageIsLogged("SQL Server is now ready for client connections"))
    .Build();

EF Core 進階功能測試

涵蓋 Include/ThenInclude 多層關聯查詢、AsSplitQuery 避免笛卡兒積、N+1 查詢問題驗證、AsNoTracking 唯讀查詢最佳化等完整測試範例。

完整程式碼範例請參考 references/orm-advanced-testing.md

Dapper 進階功能測試

涵蓋基本 CRUD 測試類別設置、QueryMultiple 一對多關聯處理、DynamicParameters 動態查詢建構等完整測試範例。

完整程式碼範例請參考 references/orm-advanced-testing.md

Repository Pattern 設計原則

介面分離原則 (ISP) 的應用

/// <summary>
/// 基礎 CRUD 操作介面
/// </summary>
public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAllAsync();
    Task<Product?> GetByIdAsync(int id);
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(int id);
}

/// <summary>
/// EF Core 特有的進階功能介面
/// </summary>
public interface IProductByEFCoreRepository
{
    Task<Product?> GetProductWithCategoryAndTagsAsync(int productId);
    Task<IEnumerable<Product>> GetProductsByCategoryWithSplitQueryAsync(int categoryId);
    Task<int> BatchUpdateProductPricesAsync(int categoryId, decimal priceMultiplier);
    Task<IEnumerable<Product>> GetProductsWithNoTrackingAsync(decimal minPrice);
}

/// <summary>
/// Dapper 特有的進階功能介面
/// </summary>
public interface IProductByDapperRepository
{
    Task<Product?> GetProductWithTagsAsync(int productId);
    Task<IEnumerable<Product>> SearchProductsAsync(int? categoryId, decimal? minPrice, bool? isActive);
    Task<IEnumerable<ProductSalesReport>> GetProductSalesReportAsync(decimal minPrice);
}

設計優勢

  1. 單一職責原則 (SRP):每個介面專注於特定職責
  2. 介面隔離原則 (ISP):使用者只需依賴所需的介面
  3. 依賴反轉原則 (DIP):高層模組依賴抽象而非具體實作
  4. 測試隔離性:可針對特定功能進行精準測試

常見問題處理

Docker 容器啟動失敗

# 檢查連接埠是否被佔用
netstat -an | findstr :5432

# 清理未使用的映像檔
docker system prune -a

記憶體不足問題

  • 調整 Docker Desktop 記憶體配置
  • 限制同時執行的容器數量
  • 使用 Collection Fixture 共享容器

測試資料隔離

public void Dispose()
{
    // 按照外鍵約束順序清理資料
    _dbContext.Database.ExecuteSqlRaw("DELETE FROM OrderItems");
    _dbContext.Database.ExecuteSqlRaw("DELETE FROM Orders");
    _dbContext.Database.ExecuteSqlRaw("DELETE FROM Products");
    _dbContext.Database.ExecuteSqlRaw("DELETE FROM Categories");
    _dbContext.Dispose();
}

輸出格式

  • 產生使用 Testcontainers 的 xUnit 整合測試類別(.cs 檔案)
  • 包含 IAsyncLifetime 容器生命週期管理程式碼
  • 包含 Collection Fixture 共享容器設定
  • 產生外部化 SQL 腳本檔案(.sql)與對應的 .csproj 設定
  • 包含 Repository Pattern 介面與實作範例

參考資源

原始文章

本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:

官方文件

相關技能

  • dotnet-testing-advanced-testcontainers-nosql - NoSQL 容器測試(MongoDB、Redis)
  • dotnet-testing-advanced-webapi-integration-testing - 完整 WebAPI 整合測試
  • dotnet-testing-advanced-aspnet-integration-testing - ASP.NET Core 基礎整合測試

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

dotnet-testing-code-coverage-analysis

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-advanced-webapi-integration-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-unit-test-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review