Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.
Testcontainers Database Integration Testing Guide
Applicable Scenarios
Use this skill when asked to perform the following tasks:
- Need to test real database behavior (transactions, concurrency, stored procedures, etc.)
- EF Core InMemory database cannot meet testing requirements
- Create containerized testing environment for PostgreSQL or MSSQL
- Use Collection Fixture pattern to share container instances
- Test data access layer for both EF Core and Dapper
- Need SQL script externalization strategy
EF Core InMemory Limitations
Before choosing a testing strategy, must understand the significant limitations of EF Core InMemory database:
1. Transaction Behavior and Database Locking
- Does not support database transactions:
SaveChanges() immediately saves data, cannot Rollback
- No database locking mechanism: Cannot simulate behavior under concurrency scenarios
2. LINQ Query Differences
- Query translation differences: Some LINQ queries (complex GroupBy, JOIN, custom functions) work in InMemory but
may fail when translated to SQL
- Case Sensitivity: InMemory defaults to case-insensitive, but real databases depend on collation rules
- Performance simulation insufficient: Cannot simulate real database performance bottlenecks or index issues
3. Database-Specific Features
InMemory mode cannot test:
- Stored Procedures and Triggers
- Views
- Foreign Key Constraints, Check Constraints
- Data type precision (decimal, datetime, etc.)
- Concurrency Tokens (RowVersion, Timestamp)
Conclusion: When needing to validate complex transaction logic, concurrency handling, or database-specific behavior,
should use Testcontainers for integration testing.
Testcontainers Core Concepts
What is Testcontainers?
Testcontainers is a testing library providing lightweight, easy-to-use API to start Docker containers, specifically for
integration testing.
Core Advantages
- Real Environment Testing: Use real databases, test actual SQL syntax and database constraints
- Environment Consistency: Ensure test environment uses same service versions as production
- Clean Test Environment: Each test has independent clean environment, containers auto-cleanup
- Simplified Development Environment: Developers only need Docker, no need to install various services
Required Packages
<ItemGroup>
<!-- Test frameworks -->
<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 core packages -->
<PackageReference Include="Testcontainers" Version="3.10.0" />
<!-- Database containers -->
<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 (optional) -->
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>
```text
> **Important**: Use `Microsoft.Data.SqlClient` instead of the old `System.Data.SqlClient` for better performance and security.
## Environment Requirements
### Docker Desktop Configuration
- Windows 10 version 2004 or later
- WSL 2 feature enabled
- 8GB RAM (16GB+ recommended)
- 64GB available disk space
### Recommended Docker Desktop Resources Settings
- Memory: 6GB (50-75% of system memory)
- CPUs: 4 cores
- Swap: 2GB
- Disk image size: 64GB
## Basic Container Operation Patterns
### PostgreSQL Container
```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) // Auto-assign host port
.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();
}
}
```text
### SQL Server Container
```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();
}
}
```text
## Collection Fixture Pattern: Container Sharing
### Why Container Sharing?
In large projects, creating new containers for each test class encounters serious performance bottlenecks:
- **Traditional approach**: Each test class starts a container. If there are 3 test classes, total time is approximately `3 × 10 seconds = 30 seconds`
- **Collection Fixture**: All test classes share the same container. Total time is only approximately `1 × 10 seconds = 10 seconds`
### Test execution time reduced by approximately 67%
### Collection Fixture Implementation
```csharp
/// <summary>
/// MSSQL container 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();
// Wait for container to fully start
await Task.Delay(2000);
}
public async Task DisposeAsync()
{
await _container.DisposeAsync();
}
}
/// <summary>
/// Define test collection
/// </summary>
[CollectionDefinition(nameof(SqlServerCollectionFixture))]
public class SqlServerCollectionFixture : ICollectionFixture<SqlServerContainerFixture>
{
// This class is just for defining Collection, no implementation needed
}
```text
### Test Class Integration
```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()
{
// Clean up data in foreign key constraint order
_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();
}
}
```text
## SQL Script Externalization Strategy
### Why Externalize SQL Scripts?
- **Separation of Concerns**: C# code focuses on test logic, SQL scripts focus on database structure
- **Maintainability**: When modifying database structure, only edit `.sql` files
- **Readability**: C# code becomes cleaner
- **Tool Support**: SQL files get editor syntax highlighting and formatting support
- **Version Control Friendly**: SQL changes can be clearly tracked in version control
### Folder Structure
```text
tests/DatabaseTesting.Tests/
├── SqlScripts/
│ ├── Tables/
│ │ ├── CreateCategoriesTable.sql
│ │ ├── CreateProductsTable.sql
│ │ ├── CreateOrdersTable.sql
│ │ └── CreateOrderItemsTable.sql
│ └── StoredProcedures/
│ └── GetProductSalesReport.sql
```text
### .csproj Configuration
```xml
<ItemGroup>
<Content Include="SqlScripts\**\*.sql">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
```text
### Script Loading Implementation
```csharp
private void EnsureTablesExist()
{
var scriptDirectory = Path.Combine(AppContext.BaseDirectory, "SqlScripts");
if (!Directory.Exists(scriptDirectory)) return;
// Execute table creation scripts in dependency order
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);
}
}
}
```text
## Wait Strategy Best Practices
### Built-in Wait Strategy
```csharp
// Wait for specific port to be available
var postgres = new PostgreSqlBuilder()
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(5432))
.Build();
// Wait for log message to appear
var sqlServer = new MsSqlBuilder()
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(1433)
.UntilMessageIsLogged("SQL Server is now ready for client connections"))
.Build();
```text
## EF Core Advanced Feature Testing
Covers Include/ThenInclude multi-level relationship queries, AsSplitQuery to avoid Cartesian product, N+1 query problem validation, AsNoTracking read-only query optimization, and other complete testing examples.
> 📖 Complete code examples please refer to [references/orm-advanced-testing.md](references/orm-advanced-testing.md#ef-core-advanced-feature-testing)
## Dapper Advanced Feature Testing
Covers basic CRUD test class setup, QueryMultiple one-to-many relationship handling, DynamicParameters dynamic query construction, and other complete testing examples.
> 📖 Complete code examples please refer to [references/orm-advanced-testing.md](references/orm-advanced-testing.md#dapper-advanced-feature-testing)
## Repository Pattern Design Principles
### Interface Segregation Principle (ISP) Application
```csharp
/// <summary>
/// Basic CRUD operation interface
/// </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 specific advanced features interface
/// </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 specific advanced features interface
/// </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);
}
```text
### Design Advantages
1. **Single Responsibility Principle (SRP)**: Each interface focuses on specific responsibilities
2. **Interface Segregation Principle (ISP)**: Consumers only depend on interfaces they need
3. **Dependency Inversion Principle (DIP)**: High-level modules depend on abstractions not concrete implementations
4. **Test Isolation**: Can perform precise testing for specific features
## Common Issues Handling
### Docker Container Startup Failure
```bash
# Check if port is occupied
netstat -an | findstr :5432
# Clean up unused images
docker system prune -a
```text
### Out of Memory Issues
- Adjust Docker Desktop memory configuration
- Limit number of simultaneously running containers
- Use Collection Fixture to share containers
### Test Data Isolation
```csharp
public void Dispose()
{
// Clean up data in foreign key constraint order
_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();
}
```text
## Reference Resources
### Original Articles
This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:
- **Day 20 - Testcontainers Introduction: Using Docker to Set Up Testing Environment**
- Article: https://ithelp.ithome.com.tw/articles/10376401
- Sample code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day20
- **Day 21 - Testcontainers Integration Testing: MSSQL + EF Core and Dapper Basic Applications**
- Article: https://ithelp.ithome.com.tw/articles/10376524
- Sample code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day21
### Official Documentation
- [Testcontainers Official Website](https://testcontainers.com/)
- [Testcontainers for .NET](https://dotnet.testcontainers.org/)
- [Testcontainers for .NET / Modules](https://dotnet.testcontainers.org/modules/)
1---2name: dotnet-testing-advanced-testcontainers-database3category: testing4subcategory: integration5description: |6 Specialized skill for containerized database testing using Testcontainers. Use when testing real database behavior, using SQL Server/PostgreSQL/MySQL containers, testing EF Core/Dapper. Covers container startup, database migrations, test isolation, container sharing.7 Keywords: testcontainers, container testing, database testing, MsSqlContainer, PostgreSqlContainer, MySqlContainer, EF Core testing, Dapper testing, Testcontainers.MsSql, Testcontainers.PostgreSql, GetConnectionString, IAsyncLifetime, CollectionFixture8targets: ['*']9license: MIT10metadata:11 author: Kevin Tseng12 version: '1.0.0'13 tags: '.NET, testing, Testcontainers, database, SQL Server, PostgreSQL'14 related_skills: 'advanced-testcontainers-nosql, advanced-aspnet-integration-testing, advanced-aspire-testing'15claudecode: {}16opencode: {}17codexcli:18 short-description: '.NET skill guidance for dotnet-testing-advanced-testcontainers-database'19copilot: {}20geminicli: {}21antigravity: {}22---2324Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.2526# Testcontainers Database Integration Testing Guide2728## Applicable Scenarios2930Use this skill when asked to perform the following tasks:3132- Need to test real database behavior (transactions, concurrency, stored procedures, etc.)33- EF Core InMemory database cannot meet testing requirements34- Create containerized testing environment for PostgreSQL or MSSQL35- Use Collection Fixture pattern to share container instances36- Test data access layer for both EF Core and Dapper37- Need SQL script externalization strategy3839## EF Core InMemory Limitations4041Before choosing a testing strategy, must understand the significant limitations of EF Core InMemory database:4243### 1. Transaction Behavior and Database Locking4445- **Does not support database transactions**: `SaveChanges()` immediately saves data, cannot Rollback46- **No database locking mechanism**: Cannot simulate behavior under concurrency scenarios4748### 2. LINQ Query Differences4950- **Query translation differences**: Some LINQ queries (complex GroupBy, JOIN, custom functions) work in InMemory but51 may fail when translated to SQL52- **Case Sensitivity**: InMemory defaults to case-insensitive, but real databases depend on collation rules53- **Performance simulation insufficient**: Cannot simulate real database performance bottlenecks or index issues5455### 3. Database-Specific Features5657InMemory mode cannot test:5859- Stored Procedures and Triggers60- Views61- Foreign Key Constraints, Check Constraints62- Data type precision (decimal, datetime, etc.)63- Concurrency Tokens (RowVersion, Timestamp)6465**Conclusion**: When needing to validate complex transaction logic, concurrency handling, or database-specific behavior,66should use Testcontainers for integration testing.6768## Testcontainers Core Concepts6970### What is Testcontainers?7172Testcontainers is a testing library providing lightweight, easy-to-use API to start Docker containers, specifically for73integration testing.7475### Core Advantages76771. **Real Environment Testing**: Use real databases, test actual SQL syntax and database constraints782. **Environment Consistency**: Ensure test environment uses same service versions as production793. **Clean Test Environment**: Each test has independent clean environment, containers auto-cleanup804. **Simplified Development Environment**: Developers only need Docker, no need to install various services8182## Required Packages8384````xml85<ItemGroup>86 <!-- Test frameworks -->87 <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />88 <PackageReference Include="xunit" Version="2.9.3" />89 <PackageReference Include="xunit.runner.visualstudio" Version="2.9.3" />90 <PackageReference Include="AwesomeAssertions" Version="9.1.0" />9192 <!-- Testcontainers core packages -->93 <PackageReference Include="Testcontainers" Version="3.10.0" />9495 <!-- Database containers -->96 <PackageReference Include="Testcontainers.PostgreSql" Version="3.10.0" />97 <PackageReference Include="Testcontainers.MsSql" Version="3.10.0" />9899 <!-- Entity Framework Core -->100 <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />101 <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />102 <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />103104 <!-- Dapper (optional) -->105 <PackageReference Include="Dapper" Version="2.1.35" />106 <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />107</ItemGroup>108```text109110> **Important**: Use `Microsoft.Data.SqlClient` instead of the old `System.Data.SqlClient` for better performance and security.111112## Environment Requirements113114### Docker Desktop Configuration115116- Windows 10 version 2004 or later117- WSL 2 feature enabled118- 8GB RAM (16GB+ recommended)119- 64GB available disk space120121### Recommended Docker Desktop Resources Settings122123- Memory: 6GB (50-75% of system memory)124- CPUs: 4 cores125- Swap: 2GB126- Disk image size: 64GB127128## Basic Container Operation Patterns129130### PostgreSQL Container131132```csharp133public class PostgreSqlTests : IAsyncLifetime134{135 private readonly PostgreSqlContainer _postgres;136 private UserDbContext _dbContext = null!;137138 public PostgreSqlTests()139 {140 _postgres = new PostgreSqlBuilder()141 .WithImage("postgres:15-alpine")142 .WithDatabase("testdb")143 .WithUsername("testuser")144 .WithPassword("testpass")145 .WithPortBinding(5432, true) // Auto-assign host port146 .Build();147 }148149 public async Task InitializeAsync()150 {151 await _postgres.StartAsync();152153 var options = new DbContextOptionsBuilder<UserDbContext>()154 .UseNpgsql(_postgres.GetConnectionString())155 .Options;156157 _dbContext = new UserDbContext(options);158 await _dbContext.Database.EnsureCreatedAsync();159 }160161 public async Task DisposeAsync()162 {163 await _dbContext.DisposeAsync();164 await _postgres.DisposeAsync();165 }166}167```text168169### SQL Server Container170171```csharp172public class SqlServerTests : IAsyncLifetime173{174 private readonly MsSqlContainer _container;175 private UserDbContext _dbContext = null!;176177 public SqlServerTests()178 {179 _container = new MsSqlBuilder()180 .WithImage("mcr.microsoft.com/mssql/server:2022-latest")181 .WithPassword("YourStrong@Passw0rd")182 .WithCleanUp(true)183 .Build();184 }185186 public async Task InitializeAsync()187 {188 await _container.StartAsync();189190 var options = new DbContextOptionsBuilder<UserDbContext>()191 .UseSqlServer(_container.GetConnectionString())192 .Options;193194 _dbContext = new UserDbContext(options);195 await _dbContext.Database.EnsureCreatedAsync();196 }197198 public async Task DisposeAsync()199 {200 await _dbContext.DisposeAsync();201 await _container.DisposeAsync();202 }203}204```text205206## Collection Fixture Pattern: Container Sharing207208### Why Container Sharing?209210In large projects, creating new containers for each test class encounters serious performance bottlenecks:211212- **Traditional approach**: Each test class starts a container. If there are 3 test classes, total time is approximately `3 × 10 seconds = 30 seconds`213- **Collection Fixture**: All test classes share the same container. Total time is only approximately `1 × 10 seconds = 10 seconds`214215### Test execution time reduced by approximately 67%216217### Collection Fixture Implementation218219```csharp220/// <summary>221/// MSSQL container Collection Fixture222/// </summary>223public class SqlServerContainerFixture : IAsyncLifetime224{225 private readonly MsSqlContainer _container;226227 public SqlServerContainerFixture()228 {229 _container = new MsSqlBuilder()230 .WithImage("mcr.microsoft.com/mssql/server:2022-latest")231 .WithPassword("Test123456!")232 .WithCleanUp(true)233 .Build();234 }235236 public static string ConnectionString { get; private set; } = string.Empty;237238 public async Task InitializeAsync()239 {240 await _container.StartAsync();241 ConnectionString = _container.GetConnectionString();242243 // Wait for container to fully start244 await Task.Delay(2000);245 }246247 public async Task DisposeAsync()248 {249 await _container.DisposeAsync();250 }251}252253/// <summary>254/// Define test collection255/// </summary>256[CollectionDefinition(nameof(SqlServerCollectionFixture))]257public class SqlServerCollectionFixture : ICollectionFixture<SqlServerContainerFixture>258{259 // This class is just for defining Collection, no implementation needed260}261```text262263### Test Class Integration264265```csharp266[Collection(nameof(SqlServerCollectionFixture))]267public class EfCoreTests : IDisposable268{269 private readonly ECommerceDbContext _dbContext;270271 public EfCoreTests(ITestOutputHelper testOutputHelper)272 {273 var connectionString = SqlServerContainerFixture.ConnectionString;274275 var options = new DbContextOptionsBuilder<ECommerceDbContext>()276 .UseSqlServer(connectionString)277 .EnableSensitiveDataLogging()278 .LogTo(testOutputHelper.WriteLine, LogLevel.Information)279 .Options;280281 _dbContext = new ECommerceDbContext(options);282 _dbContext.Database.EnsureCreated();283 }284285 public void Dispose()286 {287 // Clean up data in foreign key constraint order288 _dbContext.Database.ExecuteSqlRaw("DELETE FROM OrderItems");289 _dbContext.Database.ExecuteSqlRaw("DELETE FROM Orders");290 _dbContext.Database.ExecuteSqlRaw("DELETE FROM Products");291 _dbContext.Database.ExecuteSqlRaw("DELETE FROM Categories");292 _dbContext.Dispose();293 }294}295```text296297## SQL Script Externalization Strategy298299### Why Externalize SQL Scripts?300301- **Separation of Concerns**: C# code focuses on test logic, SQL scripts focus on database structure302- **Maintainability**: When modifying database structure, only edit `.sql` files303- **Readability**: C# code becomes cleaner304- **Tool Support**: SQL files get editor syntax highlighting and formatting support305- **Version Control Friendly**: SQL changes can be clearly tracked in version control306307### Folder Structure308309```text310tests/DatabaseTesting.Tests/311├── SqlScripts/312│ ├── Tables/313│ │ ├── CreateCategoriesTable.sql314│ │ ├── CreateProductsTable.sql315│ │ ├── CreateOrdersTable.sql316│ │ └── CreateOrderItemsTable.sql317│ └── StoredProcedures/318│ └── GetProductSalesReport.sql319```text320321### .csproj Configuration322323```xml324<ItemGroup>325 <Content Include="SqlScripts\**\*.sql">326 <CopyToOutputDirectory>Always</CopyToOutputDirectory>327 </Content>328</ItemGroup>329```text330331### Script Loading Implementation332333```csharp334private void EnsureTablesExist()335{336 var scriptDirectory = Path.Combine(AppContext.BaseDirectory, "SqlScripts");337 if (!Directory.Exists(scriptDirectory)) return;338339 // Execute table creation scripts in dependency order340 var orderedScripts = new[]341 {342 "Tables/CreateCategoriesTable.sql",343 "Tables/CreateProductsTable.sql",344 "Tables/CreateOrdersTable.sql",345 "Tables/CreateOrderItemsTable.sql"346 };347348 foreach (var scriptPath in orderedScripts)349 {350 var fullPath = Path.Combine(scriptDirectory, scriptPath);351 if (File.Exists(fullPath))352 {353 var script = File.ReadAllText(fullPath);354 _dbContext.Database.ExecuteSqlRaw(script);355 }356 }357}358```text359360## Wait Strategy Best Practices361362### Built-in Wait Strategy363364```csharp365// Wait for specific port to be available366var postgres = new PostgreSqlBuilder()367 .WithWaitStrategy(Wait.ForUnixContainer()368 .UntilPortIsAvailable(5432))369 .Build();370371// Wait for log message to appear372var sqlServer = new MsSqlBuilder()373 .WithWaitStrategy(Wait.ForUnixContainer()374 .UntilPortIsAvailable(1433)375 .UntilMessageIsLogged("SQL Server is now ready for client connections"))376 .Build();377```text378379## EF Core Advanced Feature Testing380381Covers Include/ThenInclude multi-level relationship queries, AsSplitQuery to avoid Cartesian product, N+1 query problem validation, AsNoTracking read-only query optimization, and other complete testing examples.382383> 📖 Complete code examples please refer to [references/orm-advanced-testing.md](references/orm-advanced-testing.md#ef-core-advanced-feature-testing)384385## Dapper Advanced Feature Testing386387Covers basic CRUD test class setup, QueryMultiple one-to-many relationship handling, DynamicParameters dynamic query construction, and other complete testing examples.388389> 📖 Complete code examples please refer to [references/orm-advanced-testing.md](references/orm-advanced-testing.md#dapper-advanced-feature-testing)390391## Repository Pattern Design Principles392393### Interface Segregation Principle (ISP) Application394395```csharp396/// <summary>397/// Basic CRUD operation interface398/// </summary>399public interface IProductRepository400{401 Task<IEnumerable<Product>> GetAllAsync();402 Task<Product?> GetByIdAsync(int id);403 Task AddAsync(Product product);404 Task UpdateAsync(Product product);405 Task DeleteAsync(int id);406}407408/// <summary>409/// EF Core specific advanced features interface410/// </summary>411public interface IProductByEFCoreRepository412{413 Task<Product?> GetProductWithCategoryAndTagsAsync(int productId);414 Task<IEnumerable<Product>> GetProductsByCategoryWithSplitQueryAsync(int categoryId);415 Task<int> BatchUpdateProductPricesAsync(int categoryId, decimal priceMultiplier);416 Task<IEnumerable<Product>> GetProductsWithNoTrackingAsync(decimal minPrice);417}418419/// <summary>420/// Dapper specific advanced features interface421/// </summary>422public interface IProductByDapperRepository423{424 Task<Product?> GetProductWithTagsAsync(int productId);425 Task<IEnumerable<Product>> SearchProductsAsync(int? categoryId, decimal? minPrice, bool? isActive);426 Task<IEnumerable<ProductSalesReport>> GetProductSalesReportAsync(decimal minPrice);427}428```text429430### Design Advantages4314321. **Single Responsibility Principle (SRP)**: Each interface focuses on specific responsibilities4332. **Interface Segregation Principle (ISP)**: Consumers only depend on interfaces they need4343. **Dependency Inversion Principle (DIP)**: High-level modules depend on abstractions not concrete implementations4354. **Test Isolation**: Can perform precise testing for specific features436437## Common Issues Handling438439### Docker Container Startup Failure440441```bash442# Check if port is occupied443netstat -an | findstr :5432444445# Clean up unused images446docker system prune -a447```text448449### Out of Memory Issues450451- Adjust Docker Desktop memory configuration452- Limit number of simultaneously running containers453- Use Collection Fixture to share containers454455### Test Data Isolation456457```csharp458public void Dispose()459{460 // Clean up data in foreign key constraint order461 _dbContext.Database.ExecuteSqlRaw("DELETE FROM OrderItems");462 _dbContext.Database.ExecuteSqlRaw("DELETE FROM Orders");463 _dbContext.Database.ExecuteSqlRaw("DELETE FROM Products");464 _dbContext.Database.ExecuteSqlRaw("DELETE FROM Categories");465 _dbContext.Dispose();466}467```text468469## Reference Resources470471### Original Articles472473This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:474475- **Day 20 - Testcontainers Introduction: Using Docker to Set Up Testing Environment**476 - Article: https://ithelp.ithome.com.tw/articles/10376401477 - Sample code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day20478479- **Day 21 - Testcontainers Integration Testing: MSSQL + EF Core and Dapper Basic Applications**480 - Article: https://ithelp.ithome.com.tw/articles/10376524481 - Sample code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day21482483### Official Documentation484485- [Testcontainers Official Website](https://testcontainers.com/)486- [Testcontainers for .NET](https://dotnet.testcontainers.org/)487- [Testcontainers for .NET / Modules](https://dotnet.testcontainers.org/modules/)488````