home / skills / kevintsengtw / dotnet-testing-agent-skills / dotnet-testing-advanced-testcontainers-database

dotnet-testing-advanced-testcontainers-database skill

/skills/dotnet-testing-advanced-testcontainers-database

This skill enables realistic database testing with Testcontainers in .NET, orchestrating containerized SQL Server and PostgreSQL for EF Core and Dapper tests.

npx playbooks add skill kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-advanced-testcontainers-database

Review the files below or copy the command above to add this skill to your agents.

Files (6)
SKILL.md
14.7 KB
---
name: dotnet-testing-advanced-testcontainers-database
description: |
  使用 Testcontainers 進行容器化資料庫測試的專門技能。當需要測試真實資料庫行為、使用 SQL Server/PostgreSQL/MySQL 容器、測試 EF Core/Dapper 時使用。涵蓋容器啟動、資料庫遷移、測試隔離、容器共享等。
  Keywords: testcontainers, 容器測試, container testing, database testing, 資料庫測試, MsSqlContainer, PostgreSqlContainer, MySqlContainer, EF Core testing, Dapper testing, Testcontainers.MsSql, Testcontainers.PostgreSql, GetConnectionString, IAsyncLifetime, CollectionFixture
license: MIT
metadata:
  author: Kevin Tseng
  version: "1.0.0"
  tags: ".NET, testing, Testcontainers, database, SQL Server, PostgreSQL"
  related_skills: "advanced-testcontainers-nosql, advanced-aspnet-integration-testing, advanced-aspire-testing"
---

# Testcontainers 資料庫整合測試指南

## 適用情境

當被要求執行以下任務時,請使用此技能:

- 需要測試真實資料庫行為(交易、並發、預存程序等)
- EF Core InMemory 資料庫無法滿足測試需求
- 建立 PostgreSQL 或 MSSQL 的容器化測試環境
- 使用 Collection Fixture 模式共享容器實例
- 同時測試 EF Core 和 Dapper 的資料存取層
- 需要 SQL 腳本外部化策略

## 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,不需安裝各種服務

## 必要套件

```xml
<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 容器

```csharp
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 容器

```csharp
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 實作

```csharp
/// <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,不需要實作內容
}
```

### 測試類別整合

```csharp
[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 變更可在版本控制系統中清楚追蹤

### 資料夾結構

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

### .csproj 設定

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

### 腳本載入實作

```csharp
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

```csharp
// 等待特定埠號可用
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](references/orm-advanced-testing.md#ef-core-進階功能測試)

## Dapper 進階功能測試

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

> 📖 完整程式碼範例請參考 [references/orm-advanced-testing.md](references/orm-advanced-testing.md#dapper-進階功能測試)

## Repository Pattern 設計原則

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

```csharp
/// <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 容器啟動失敗

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

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

### 記憶體不足問題

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

### 測試資料隔離

```csharp
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();
}
```

## 參考資源

### 原始文章

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

- **Day 20 - Testcontainers 初探:使用 Docker 架設測試環境**
  - 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10376401
  - 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day20

- **Day 21 - Testcontainers 整合測試:MSSQL + EF Core 以及 Dapper 基礎應用**
  - 鐵人賽文章:https://ithelp.ithome.com.tw/articles/10376524
  - 範例程式碼:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day21

### 官方文件

- [Testcontainers 官方網站](https://testcontainers.com/)
- [Testcontainers for .NET](https://dotnet.testcontainers.org/)
- [Testcontainers for .NET / Modules](https://dotnet.testcontainers.org/modules/)

Overview

This skill teaches how to use Testcontainers to run containerized database integration tests for SQL Server, PostgreSQL, and MySQL. It focuses on realistic database behavior testing for EF Core and Dapper, covering container lifecycle, migrations, isolation, and container sharing strategies. The guidance is practical, with examples for fixtures, scripts, wait strategies, and cleanup.

How this skill works

The skill shows how to start disposable database containers using Testcontainers builders, obtain connection strings, and wire them into DbContext or Dapper connections. It covers IAsyncLifetime-based fixtures and xUnit CollectionFixture to share containers across test classes. It also demonstrates applying SQL migration scripts, wait strategies, and deterministic teardown to keep tests isolated and repeatable.

When to use it

  • You need to test real database behaviors (transactions, locking, stored procedures).
  • EF Core InMemory cannot reproduce SQL translation, collation, or constraint behavior.
  • You want to run integration tests against SQL Server, PostgreSQL, or MySQL containers.
  • You need to test both EF Core and Dapper data access code against the same DB engine.
  • You want faster test suites by sharing containers with CollectionFixture.

Best practices

  • Use IAsyncLifetime or CollectionFixture to manage container lifecycle and reduce startup overhead.
  • Externalize SQL schema and seed scripts under a SqlScripts folder and copy them to output.
  • Apply explicit wait strategies (port availability and log messages) before running tests.
  • Clean test data in Dispose or per-test teardown following foreign-key order to maintain isolation.
  • Prefer Microsoft.Data.SqlClient for SQL Server and proper provider packages for EF Core.

Example use cases

  • Verify transactional rollback, concurrency tokens, and locking behavior using a real database container.
  • Run EF Core advanced query tests (Include/ThenInclude, AsSplitQuery, AsNoTracking) against PostgreSQL.
  • Exercise Dapper QueryMultiple and DynamicParameters for one-to-many mappings in integration tests.
  • Share a single SQL Server container across multiple test classes to cut CI time.
  • Load and run ordered SQL table creation scripts from test output to set up schema reliably.

FAQ

Why not use EF Core InMemory for all tests?

InMemory misses transaction semantics, collation differences, SQL translation mismatches, and database-specific features like stored procedures and triggers—use Testcontainers when you need real DB behavior.

How do I speed up tests that need containers?

Share containers with xUnit CollectionFixture, reuse images, and use targeted wait strategies to avoid unnecessary delays.