在 Day 29 中,我們深入探討了 TUnit 的資料驅動測試和依賴注入功能。從 MethodDataSource 的靈活應用,到 ClassDataSource 的重用策略,再到 Matrix Tests 的組合威力,我們建立了完整的測試資料基礎。同時,透過 TUnit 原生的依賴注入機制,我們學會了如何在測試中有效處理複雜的服務依賴關係。
但是,在正式的真實專案中,測試不僅僅是驗證單一功能的正確性。我們還需要面對更複雜的挑戰:
進階測試挑戰:
今天我們要探討 TUnit 的執行控制機制和 ASP.NET Core 整合測試,這些功能讓我們能夠建立更可靠、更接近真實使用場景的測試套件。從智慧重試策略到完整的 Web 應用程式測試,我們將學習如何在實際的工作專案中確保軟體品質。
今天的內容有:
TUnit 的進階功能正是為了解決這些挑戰而設計的。今天我們要深入探討執行控制和整合測試等實務技巧,讓你能夠在實際的工作專案中有效運用這個框架。
在實際的工作專案中,測試的執行控制和品質管理相當重要。TUnit 提供了多種機制來幫助我們處理不穩定的測試、效能要求,以及提升測試報告的可讀性。
在真實世界中,某些測試可能會因為外部因素(如網路延遲、資源競爭)而偶爾失敗。對於這類「不穩定的測試」,盲目地重新執行整個測試套件既浪費時間又不能解決根本問題。TUnit 的 Retry 機制提供了精確的重試控制。
[Test]
[Retry(3)] // 如果失敗,重試最多 3 次
[Property("Category", "Flaky")]
public async Task NetworkCall_可能不穩定_使用重試機制()
{
// 模擬可能失敗的網路呼叫
var random = new Random();
var success = random.Next(1, 4) == 1; // 約 33% 的成功率
if (!success)
{
throw new HttpRequestException("模擬網路錯誤");
}
await Assert.That(success).IsTrue();
}
適合使用 Retry 的情況:
不適合使用 Retry 的情況:
[Test]
[Retry(3)]
[Property("Category", "ExternalDependency")]
public async Task CallExternalApi_網路問題時重試_最終應成功()
{
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
try
{
// 實際的外部 API 呼叫
var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
await Assert.That(response.IsSuccessStatusCode).IsTrue();
var content = await response.Content.ReadAsStringAsync();
await Assert.That(content).IsNotNull();
}
catch (TaskCanceledException)
{
// 超時也算是暫時性錯誤,可以重試
throw new HttpRequestException("請求超時,將重試");
}
}
// 反例:不應該用 Retry 的情況
[Test]
// 不要對預期會失敗的測試使用 Retry
public async Task Divide_被零除_應拋出例外()
{
await Assert.That(() => 10 / 0).Throws<DivideByZeroException>();
}
效能是現代應用程式的重要指標。TUnit 的 Timeout 功能讓我們能夠確保測試在合理時間內完成,避免無限期等待或效能回歸。
[Test]
[Timeout(5000)] // 5 秒超時
[Property("Category", "Performance")]
public async Task LongRunningOperation_應在時限內完成()
{
// 模擬可能會很慢的操作
await Task.Delay(1000); // 1 秒操作,應該在 5 秒限制內
await Assert.That(true).IsTrue();
}
[Test]
[Timeout(30000)] // 30 秒超時,適合較複雜的操作
[Property("Category", "Integration")]
public async Task DatabaseMigration_大量資料處理_應在合理時間內完成()
{
// 模擬資料庫遷移或大量資料處理
var tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
tasks.Add(ProcessDataBatch(i));
}
await Task.WhenAll(tasks);
await Assert.That(tasks.All(t => t.IsCompletedSuccessfully)).IsTrue();
}
private static async Task ProcessDataBatch(int batchNumber)
{
// 模擬批次處理
await Task.Delay(50); // 每批次 50ms
}
Timeout 結合效能測量,可以建立效能基準:
[Test]
[Timeout(1000)] // 確保不會超過 1 秒
[Property("Category", "Performance")]
[Property("Baseline", "true")]
public async Task SearchFunction_效能基準_應符合SLA要求()
{
var stopwatch = Stopwatch.StartNew();
// 模擬搜尋功能
var searchResults = await PerformSearch("test query");
stopwatch.Stop();
// 功能性驗證
await Assert.That(searchResults).IsNotNull();
await Assert.That(searchResults.Count()).IsGreaterThan(0);
// 效能驗證:99% 的查詢應在 500ms 內完成
await Assert.That(stopwatch.ElapsedMilliseconds).IsLessThan(500);
}
private static async Task<IEnumerable<string>> PerformSearch(string query)
{
// 模擬搜尋邏輯
await Task.Delay(100);
return new[] { "result1", "result2", "result3" };
}
清楚的測試名稱對於測試報告的可讀性很重要。TUnit 的 DisplayName 功能讓我們能夠提供更友善的測試名稱,特別是在參數化測試中。
[Test]
[DisplayName("自訂測試名稱:驗證使用者註冊流程")]
public async Task UserRegistration_CustomDisplayName_測試名稱更易讀()
{
// 使用自訂顯示名稱讓測試報告更容易理解
await Assert.That("user@example.com").Contains("@");
}
DisplayName 的主要優勢在於參數化測試,它可以自動替換參數值:
[Test]
[Arguments("valid@email.com", true)]
[Arguments("invalid-email", false)]
[Arguments("", false)]
[Arguments("test@domain.co.uk", true)]
[Arguments("user.name+tag@example.com", true)]
[DisplayName("電子郵件驗證:{0} 應為 {1}")]
public async Task EmailValidation_參數化顯示名稱(string email, bool expectedValid)
{
// 顯示名稱會自動替換參數
// 產生的名稱如:「電子郵件驗證:valid@email.com 應為 True」
var isValid = !string.IsNullOrEmpty(email) && email.Contains("@") && email.Contains(".");
await Assert.That(isValid).IsEqualTo(expectedValid);
}
在測試業務邏輯時,使用業務語言而非技術術語能讓測試報告更有意義:
[Test]
[Arguments(CustomerLevel.一般會員, 1000, 0)]
[Arguments(CustomerLevel.VIP會員, 1000, 50)]
[Arguments(CustomerLevel.白金會員, 1000, 100)]
[Arguments(CustomerLevel.鑽石會員, 1000, 200)]
[DisplayName("會員等級 {0} 購買 ${1} 應獲得 ${2} 折扣")]
public async Task MemberDiscount_根據會員等級_計算正確折扣(CustomerLevel level, decimal amount, decimal expectedDiscount)
{
// 這樣的測試報告讀起來像業務需求
var calculator = new DiscountCalculator();
var discount = await calculator.CalculateDiscountAsync(amount, level);
await Assert.That(discount).IsEqualTo(expectedDiscount);
}
整合測試是驗證應用程式各個元件協同工作的重要手段。與單元測試專注於個別元件不同,整合測試要確保整個系統能夠正確運作。在 Web 應用程式開發中,這意味著測試從 HTTP 請求到資料庫存取的完整流程。
TUnit 與 ASP.NET Core 的整合測試能力讓我們能夠測試完整的 Web 應用程式流程。這對於確保系統整體行為的正確性非常重要,特別是在微服務架構中,不同服務間的協作更需要透過整合測試來保證品質。
WebApplicationFactory 是 ASP.NET Core 提供的測試基礎設施,它能夠在記憶體中啟動完整的 Web 應用程式進行測試。
public class WebApiIntegrationTests : IDisposable
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public WebApiIntegrationTests()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// 移除原有的資料庫設定(如果有的話)
// 這裡可以加入測試專用的服務設定
services.AddLogging();
});
});
_client = _factory.CreateClient();
}
[Test]
public async Task WeatherForecast_Get_應回傳正確格式的資料()
{
// Act
var response = await _client.GetAsync("/weatherforecast");
// Assert
await Assert.That(response.IsSuccessStatusCode).IsTrue();
var content = await response.Content.ReadAsStringAsync();
await Assert.That(content).IsNotNull();
await Assert.That(content.Length).IsGreaterThan(0);
}
public void Dispose()
{
_client?.Dispose();
_factory?.Dispose();
}
}
整合測試不僅要驗證功能,還要確保 HTTP 層面的正確性:
[Test]
[Property("Category", "Integration")]
public async Task WeatherForecast_ResponseHeaders_應包含ContentType標頭()
{
// Act
var response = await _client.GetAsync("/weatherforecast");
// Assert
await Assert.That(response.IsSuccessStatusCode).IsTrue();
// 檢查實際會存在的 Content-Type 標頭
var contentType = response.Content.Headers.ContentType?.MediaType;
await Assert.That(contentType).IsEqualTo("application/json");
}
在進行複雜的效能測試之前,我們需要先確保基本功能正常運作。冒煙測試是最基礎但也是最重要的整合測試類型:
[Test]
[Property("Category", "Smoke")]
public async Task WeatherForecast_端點可用性_應能正常回應()
{
// 基本的冒煙測試:確保端點可用
// Act
var response = await _client.GetAsync("/weatherforecast");
// Assert
await Assert.That(response.IsSuccessStatusCode).IsTrue();
var content = await response.Content.ReadAsStringAsync();
await Assert.That(content).IsNotNull();
await Assert.That(content.Length).IsGreaterThan(10); // 確保有實際內容
}
冒煙測試的價值:
整合測試也是進行效能驗證的好時機:
[Test]
[Property("Category", "Performance")]
[Timeout(10000)] // 10 秒超時保護
public async Task WeatherForecast_ResponseTime_應在合理範圍內()
{
// Arrange
var stopwatch = Stopwatch.StartNew();
// Act
var response = await _client.GetAsync("/weatherforecast");
stopwatch.Stop();
// Assert
await Assert.That(response.IsSuccessStatusCode).IsTrue();
await Assert.That(stopwatch.ElapsedMilliseconds).IsLessThan(5000); // 5秒內回應
}
[Test]
[Property("Category", "Load")]
[Timeout(30000)]
public async Task WeatherForecast_並行請求_應能正確處理()
{
// Arrange
const int concurrentRequests = 50;
var tasks = new List<Task<HttpResponseMessage>>();
// Act
for (int i = 0; i < concurrentRequests; i++)
{
tasks.Add(_client.GetAsync("/weatherforecast"));
}
var responses = await Task.WhenAll(tasks);
// Assert
await Assert.That(responses.Length).IsEqualTo(concurrentRequests);
await Assert.That(responses.All(r => r.IsSuccessStatusCode)).IsTrue();
// 清理
foreach (var response in responses)
{
response.Dispose();
}
}
現代應用程式通常需要健康檢查端點供監控系統使用:
[Test]
[Property("Category", "Health")]
public async Task HealthCheck_應回傳健康狀態()
{
// 測試應用程式的健康狀態
// 這對於 Kubernetes 部署和監控很重要
try
{
var response = await _client.GetAsync("/health");
// 如果有 health endpoint 就測試,沒有就測試基本端點
await Assert.That(response.IsSuccessStatusCode).IsTrue();
}
catch (HttpRequestException)
{
// 如果沒有 /health 端點,測試根路徑
var response = await _client.GetAsync("/");
await Assert.That((int)response.StatusCode).IsLessThan(500);
}
}
整合測試的真正價值在於測試完整的業務流程:
public class OrderApiIntegrationTests : IDisposable
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public OrderApiIntegrationTests()
{
_factory = new WebApplicationFactory<Program>();
_client = _factory.CreateClient();
}
[Test]
[Property("Category", "E2E")]
[DisplayName("完整訂單流程:建立 → 查詢 → 更新狀態")]
public async Task CreateOrder_完整流程_應成功建立訂單()
{
// 這個測試展示完整的訂單建立流程
// 由於範例 API 可能沒有實際的訂單端點,我們測試基本的 API 可用性
// Act
var response = await _client.GetAsync("/");
// Assert - 確保 API 可以正常啟動和回應
// 在真實專案中,這裡會測試實際的業務邏輯端點
await Assert.That((int)response.StatusCode).IsLessThan(500); // 不是伺服器錯誤
}
public void Dispose()
{
_client?.Dispose();
_factory?.Dispose();
}
}
在正式專案中,整合測試通常需要多個外部依賴服務的協調工作。TUnit 結合 Testcontainers.NET 提供了完整的基礎設施編排能力,讓我們能夠建立接近正式環境的測試場景。
在正式專案中,最佳實踐是使用 [Before(Assembly)]
和 [After(Assembly)]
來管理容器生命週期,確保所有測試共享相同的基礎設施:
/// <summary>
/// 全域測試基礎設施設置
/// 專門處理 Assembly level 的容器管理
/// </summary>
public static class GlobalTestInfrastructureSetup
{
public static PostgreSqlContainer? PostgreSqlContainer { get; private set; }
public static RedisContainer? RedisContainer { get; private set; }
public static KafkaContainer? KafkaContainer { get; private set; }
public static INetwork? Network { get; private set; }
[Before(Assembly)]
public static async Task SetupGlobalInfrastructure()
{
Console.WriteLine("=== 開始設置全域測試基礎設施 ===");
// 建立網路
Network = new NetworkBuilder()
.WithName("global-test-network")
.Build();
await Network.CreateAsync();
Console.WriteLine($"測試網路已建立: {Network.Name}");
// 建立 PostgreSQL 容器
PostgreSqlContainer = new PostgreSqlBuilder()
.WithDatabase("test_db")
.WithUsername("test_user")
.WithPassword("test_password")
.WithNetwork(Network)
.WithCleanUp(true)
.Build();
await PostgreSqlContainer.StartAsync();
Console.WriteLine($"PostgreSQL 容器已啟動: {PostgreSqlContainer.GetConnectionString()}");
// 建立 Redis 容器
RedisContainer = new RedisBuilder()
.WithNetwork(Network)
.WithCleanUp(true)
.Build();
await RedisContainer.StartAsync();
Console.WriteLine($"Redis 容器已啟動: {RedisContainer.GetConnectionString()}");
// 建立 Kafka 容器
KafkaContainer = new KafkaBuilder()
.WithNetwork(Network)
.WithCleanUp(true)
.Build();
await KafkaContainer.StartAsync();
Console.WriteLine($"Kafka 容器已啟動: {KafkaContainer.GetBootstrapAddress()}");
Console.WriteLine("=== 全域測試基礎設施設置完成 ===");
}
[After(Assembly)]
public static async Task TeardownGlobalInfrastructure()
{
Console.WriteLine("=== 開始清理全域測試基礎設施 ===");
if (KafkaContainer != null)
{
await KafkaContainer.DisposeAsync();
Console.WriteLine("Kafka 容器已停止");
}
if (RedisContainer != null)
{
await RedisContainer.DisposeAsync();
Console.WriteLine("Redis 容器已停止");
}
if (PostgreSqlContainer != null)
{
await PostgreSqlContainer.DisposeAsync();
Console.WriteLine("PostgreSQL 容器已停止");
}
if (Network != null)
{
await Network.DeleteAsync();
Console.WriteLine("測試網路已刪除");
}
Console.WriteLine("=== 全域測試基礎設施清理完成 ===");
}
}
以下範例展示如何使用全域容器基礎設施進行基礎設施驗證測試。這些測試的目的是確保測試環境的容器服務正常運作,為後續的業務邏輯測試奠定基礎:
/// <summary>
/// 複雜測試基礎設施編排範例
/// 展示 TUnit 結合 Testcontainers.NET 的強大能力
/// 注意:這些是基礎設施驗證測試,用於確保測試環境正常運作
/// 實際的業務邏輯測試會建立在這些基礎設施之上
/// </summary>
public class ComplexInfrastructureTests
{
[Test]
[Property("Category", "Integration")]
[Property("Infrastructure", "Complex")]
[DisplayName("多服務協作:PostgreSQL + Redis + Kafka 完整測試")]
public async Task CompleteWorkflow_多服務協作_應正確執行()
{
// Arrange & Act
// 使用全域設置的容器
var dbConnectionString = GlobalTestInfrastructureSetup.PostgreSqlContainer!.GetConnectionString();
var redisConnectionString = GlobalTestInfrastructureSetup.RedisContainer!.GetConnectionString();
var kafkaBootstrapServers = GlobalTestInfrastructureSetup.KafkaContainer!.GetBootstrapAddress();
// Assert
await Assert.That(dbConnectionString).IsNotNull();
await Assert.That(dbConnectionString).Contains("test_db");
await Assert.That(dbConnectionString).Contains("test_user");
await Assert.That(redisConnectionString).IsNotNull();
await Assert.That(redisConnectionString).Contains("127.0.0.1");
await Assert.That(kafkaBootstrapServers).IsNotNull();
await Assert.That(kafkaBootstrapServers).Contains("127.0.0.1");
// 模擬完整的業務流程
Console.WriteLine("=== 多服務協作測試 ===");
Console.WriteLine($"PostgreSQL: {dbConnectionString}");
Console.WriteLine($"Redis: {redisConnectionString}");
Console.WriteLine($"Kafka: {kafkaBootstrapServers}");
Console.WriteLine("=====================");
}
[Test]
[Property("Category", "Database")]
[DisplayName("PostgreSQL 資料庫連線驗證")]
public async Task PostgreSqlDatabase_連線驗證_應成功建立連線()
{
// Arrange
var connectionString = GlobalTestInfrastructureSetup.PostgreSqlContainer!.GetConnectionString();
// Act & Assert
await Assert.That(connectionString).Contains("test_db");
await Assert.That(connectionString).Contains("test_user");
await Assert.That(connectionString).Contains("test_password");
Console.WriteLine($"Database connection verified: {connectionString}");
}
[Test]
[Property("Category", "Cache")]
[DisplayName("Redis 快取服務驗證")]
public async Task RedisCache_快取服務_應正確啟動()
{
// Arrange
var connectionString = GlobalTestInfrastructureSetup.RedisContainer!.GetConnectionString();
// Act & Assert
await Assert.That(connectionString).IsNotNull();
await Assert.That(connectionString).Contains("127.0.0.1");
Console.WriteLine($"Redis connection verified: {connectionString}");
}
[Test]
[Property("Category", "MessageQueue")]
[DisplayName("Kafka 訊息佇列服務驗證")]
public async Task KafkaMessageQueue_訊息佇列_應正確啟動()
{
// Arrange
var bootstrapServers = GlobalTestInfrastructureSetup.KafkaContainer!.GetBootstrapAddress();
// Act & Assert
await Assert.That(bootstrapServers).IsNotNull();
await Assert.That(bootstrapServers).Contains("127.0.0.1");
Console.WriteLine($"Kafka connection verified: {bootstrapServers}");
}
}
在 Assembly 級別管理的基礎上,建立進階依賴測試。以下範例展示的是測試基礎設施健康檢查,確保各服務間的網路連通性和依賴關係正常運作:
/// <summary>
/// 進階依賴管理模式範例
/// 展示 TUnit 的複雜依賴鏈管理能力
/// 注意:這些是基礎設施健康檢查測試,確保測試環境的服務依賴正常
/// 真正的業務邏輯測試會使用這些已驗證的基礎設施進行實際功能測試
/// </summary>
public class AdvancedDependencyTests
{
[Test]
[Property("Category", "Network")]
[DisplayName("網路基礎設施驗證")]
public async Task NetworkInfrastructure_網路設定_應正確建立()
{
// Arrange & Act
var networkName = GlobalTestInfrastructureSetup.Network!.Name;
// Assert
await Assert.That(networkName).IsEqualTo("global-test-network");
Console.WriteLine($"Test network verified: {networkName}");
}
[Test]
[Property("Category", "Database")]
[DisplayName("網路化資料庫服務驗證")]
public async Task NetworkedDatabase_資料庫網路_應正確設定()
{
// Arrange
var connectionString = GlobalTestInfrastructureSetup.PostgreSqlContainer!.GetConnectionString();
// Act & Assert
await Assert.That(connectionString).Contains("test_db");
await Assert.That(connectionString).Contains("test_user");
// 驗證資料庫容器在指定網路中
await Assert.That(GlobalTestInfrastructureSetup.PostgreSqlContainer.State).IsEqualTo(TestcontainersStates.Running);
Console.WriteLine($"Networked database verified: {connectionString}");
}
[Test]
[Property("Category", "Integration")]
[DisplayName("跨容器網路通訊測試")]
public async Task CrossContainerCommunication_容器間通訊_應正常運作()
{
// Arrange
var dbConnectionString = GlobalTestInfrastructureSetup.PostgreSqlContainer!.GetConnectionString();
var networkName = GlobalTestInfrastructureSetup.Network!.Name;
// Act & Assert
await Assert.That(dbConnectionString).IsNotNull();
await Assert.That(networkName).IsEqualTo("global-test-network");
Console.WriteLine("Cross-container communication test ready");
Console.WriteLine($"Network: {networkName}");
Console.WriteLine($"Database: {dbConnectionString}");
}
}
以上兩個範例(ComplexInfrastructureTests
和 AdvancedDependencyTests
)展示的是基礎設施驗證測試,目的是確保測試環境的容器服務正常運作。在實際專案中,你會在這些基礎設施之上建立真正的業務邏輯測試:
// 基礎設施驗證測試(如上方範例)
// ↓ 確保測試環境正常運作後
// ↓ 建立實際的業務邏輯測試
/// <summary>
/// 使用已驗證的基礎設施進行實際業務邏輯測試
/// </summary>
public class OrderServiceIntegrationTests
{
[Test]
public async Task CreateOrder_使用PostgreSQL和Redis_應正確處理訂單流程()
{
// 使用 GlobalTestInfrastructureSetup 提供的容器
var dbConnection = GlobalTestInfrastructureSetup.PostgreSqlContainer!.GetConnectionString();
var redisConnection = GlobalTestInfrastructureSetup.RedisContainer!.GetConnectionString();
// 建立實際的服務實例
var orderService = new OrderService(dbConnection, redisConnection);
// 執行真正的業務邏輯測試
var order = await orderService.CreateOrderAsync(new CreateOrderRequest { ... });
// 驗證業務邏輯結果
await Assert.That(order.Id).IsNotNull();
await Assert.That(order.Status).IsEqualTo(OrderStatus.Created);
}
}
這樣的分層設計讓測試架構更清晰:基礎設施驗證 → 業務邏輯測試 → 端到端測試。
Assembly 級別的容器管理提供了最佳的效能優勢:
Assembly-level 容器共享的好處:
// Assembly 級別的效能最佳化範例
[Before(Assembly)]
public static async Task SetupGlobalInfrastructure()
{
// 所有容器在整個測試會話開始時啟動一次
PostgreSqlContainer = new PostgreSqlBuilder()
.WithDatabase("test_db")
.WithUsername("test_user")
.WithPassword("test_password")
.WithNetwork(Network)
.WithCleanUp(true) // 測試結束後自動清理
.Build();
await PostgreSqlContainer.StartAsync();
}
在實際專案中,你可以建立專門的測試基礎設施管理器:
/// <summary>
/// 測試基礎設施管理器
/// 提供統一的容器管理和依賴注入
/// </summary>
public class TestInfrastructureManager
{
/// <summary>
/// 取得完整的應用程式設定
/// </summary>
private Dictionary<string, string> GetTestConfiguration()
{
return new Dictionary<string, string>
{
["ConnectionStrings:DefaultConnection"] = GlobalTestInfrastructureSetup.PostgreSqlContainer!.GetConnectionString(),
["ConnectionStrings:Redis"] = GlobalTestInfrastructureSetup.RedisContainer!.GetConnectionString(),
["Environment"] = "Testing"
};
}
[Test]
[Property("Category", "Infrastructure")]
[DisplayName("基礎設施管理器:設定產生驗證")]
public async Task InfrastructureManager_設定產生_應提供完整設定()
{
// Act
var configuration = GetTestConfiguration();
// Assert
await Assert.That(configuration).IsNotNull();
await Assert.That(configuration.ContainsKey("ConnectionStrings:DefaultConnection")).IsTrue();
await Assert.That(configuration.ContainsKey("ConnectionStrings:Redis")).IsTrue();
await Assert.That(configuration.ContainsKey("Environment")).IsTrue();
await Assert.That(configuration["Environment"]).IsEqualTo("Testing");
await Assert.That(configuration["ConnectionStrings:DefaultConnection"]).Contains("test_db");
await Assert.That(configuration["ConnectionStrings:Redis"]).Contains("127.0.0.1");
// 輸出設定資訊以供檢視
Console.WriteLine("Generated test configuration:");
foreach (var kvp in configuration)
{
Console.WriteLine($" {kvp.Key}: {kvp.Value}");
}
}
[Test]
[Property("Category", "Infrastructure")]
[DisplayName("基礎設施管理器:容器健康狀態檢查")]
public async Task InfrastructureManager_容器健康檢查_所有服務應正常運作()
{
// Act & Assert
await Assert.That(GlobalTestInfrastructureSetup.PostgreSqlContainer!.State).IsEqualTo(TestcontainersStates.Running);
await Assert.That(GlobalTestInfrastructureSetup.RedisContainer!.State).IsEqualTo(TestcontainersStates.Running);
// 驗證連線字串有效性
var dbConnection = GlobalTestInfrastructureSetup.PostgreSqlContainer.GetConnectionString();
var redisConnection = GlobalTestInfrastructureSetup.RedisContainer.GetConnectionString();
await Assert.That(dbConnection).IsNotNull();
await Assert.That(redisConnection).IsNotNull();
Console.WriteLine("Infrastructure health check passed:");
Console.WriteLine($" Database: {GlobalTestInfrastructureSetup.PostgreSqlContainer.State} - {dbConnection}");
Console.WriteLine($" Redis: {GlobalTestInfrastructureSetup.RedisContainer.State} - {redisConnection}");
}
}
這種基礎設施編排方式讓我們能夠建立真正接近正式環境的測試場景,確保整合測試的可靠性和真實性。
在執行 TUnit 測試時,你可能注意到輸出中會顯示 Engine Mode: SourceGenerated
。這是 TUnit 的一個重要特色,讓我們深入了解這個機制。
TUnit 支援兩種執行模式,各有其適用場景和性能特色:
這是 TUnit 的預設模式,也是其核心競爭優勢所在:
████████╗██╗ ██╗███╗ ██╗██╗████████╗
╚══██╔══╝██║ ██║████╗ ██║██║╚══██╔══╝
██║ ██║ ██║██╔██╗ ██║██║ ██║
██║ ██║ ██║██║╚██╗██║██║ ██║
██║ ╚██████╔╝██║ ╚████║██║ ██║
╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
TUnit v0.57.24.0
Engine Mode: SourceGenerated
特色與優勢:
Source Generator 如何運作:
TUnit 的 Source Generator 會在編譯時建立強型別的委派:
傳統的執行時反射模式,可透過命令列參數啟用:
# 啟用反射模式
dotnet run -- --reflection
# 或設定環境變數
$env:TUNIT_EXECUTION_MODE = "reflection"
dotnet run
適用場景:
當使用 Native AOT 發佈時,Source Generation 模式提供額外的效能提升:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
dotnet publish -c Release
不同程式語言的支援狀況:
F#/VB.NET 的效能最佳化策略:
如果你的主要程式碼是 F# 或 VB.NET,但想享受 Source Generation 的效能優勢:
Solution/
├── MyLibrary.fsproj # F# 主要邏輯
├── MyLibrary.Tests.csproj # C# 測試專案(使用 Source Generation)
└── ...
這種架構讓你能夠:
對於 Source Generation 相關問題,TUnit 提供診斷選項:
# .editorconfig
# 啟用詳細診斷(預設:false)
tunit.enable_verbose_diagnostics = true
<PropertyGroup>
<TUnitEnableVerboseDiagnostics>true</TUnitEnableVerboseDiagnostics>
</PropertyGroup>
在我們的測試專案中,可以觀察到 Source Generation 模式的優勢:
# Source Generation 模式(預設)
Engine Mode: SourceGenerated
total: 25, succeeded: 25, duration: 1s 105ms
# 如果切換到反射模式(僅示意)
# dotnet run -- --reflection
Engine Mode: Reflection
total: 25, succeeded: 25, duration: 2s 340ms
Source Generation 的效能優勢在於:
在實際使用 TUnit 的過程中,你可能會遇到一些特有的問題。基於實際的開發經驗,以下是最常見的問題和解決方案:
執行 dotnet test
時出現以下情況:
測試摘要: 總計: 0, 失敗: 0, 成功: 0
TUnit 使用的是 Microsoft.Testing.Platform,而不是傳統的 VSTest 平台。這個架構差異會導致幾個問題:
測試摘要: 總計: X, 失敗: Y, 成功: Z, 已跳過: W
最常見的問題是測試專案的 Program.cs 包含了會干擾測試輸出的程式碼:
// 錯誤:會干擾測試統計顯示
Console.WriteLine("Starting application...");
var builder = WebApplication.CreateBuilder(args);
// ... 其他程式碼
// 正確:保持最簡潔的 Program.cs
var builder = WebApplication.CreateBuilder(args);
// 只包含必要的服務註冊和中介軟體設定
檢查 .csproj
檔案是否包含必要設定:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <!-- 重要:標記為測試專案 -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" Version="0.57.24" />
<!-- 整合測試需要額外套件,注意版本要與 .NET 9 相容 -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
</ItemGroup>
</Project>
確保 GlobalUsing.cs 包含所有必要的命名空間:
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using TUnit.Core;
global using TUnit.Assertions;
global using TUnit.Assertions.Extensions;
global using Microsoft.Extensions.Logging;
// 整合測試專案還需要:
global using Microsoft.AspNetCore.Mvc.Testing;
global using System.Diagnostics;
針對 ASP.NET Core 整合測試,確保 Program 類別可以被測試存取:
// 在 WebApi 專案的 Program.cs 最後加上
public partial class Program { } // 讓整合測試可以存取
執行以下命令來驗證修正效果:
# 1. 清理和重建
dotnet clean; dotnet build
# 2. 執行測試並觀察輸出格式
dotnet test --verbosity normal
# 3. 正確的輸出應該類似:
# 測試摘要: 總計: 132, 失敗: 0, 成功: 132, 已跳過: 0
dotnet clean; dotnet build
)// 調整並行設定
[Test]
[Property("Parallel", "false")] // 對於資源敏感的測試關閉並行
public async Task SlowTest_應在合理時間內完成()
{
// 測試邏輯
}
確保正確釋放資源:
public void Dispose()
{
_client?.Dispose();
_factory?.Dispose();
// 釋放其他資源
}
在我們的範例專案中,整合測試專案曾經遇到典型的問題:
問題現象:
測試摘要: 總計: 0, 失敗: 0, 成功: 0, 已跳過: 0
解決步驟:
GlobalUsing.cs
,保留標準的 GlobalUsings.cs
修正後結果:
測試摘要: 總計: 6, 失敗: 0, 成功: 6, 已跳過: 0
這個案例說明了測試設計的重要原則:測試應該驗證實際的業務需求,而不是測試框架的容錯能力。
在這篇文章中,我們探討了 TUnit 的執行控制和整合測試功能,從測試品質管理到完整的 Web 應用程式驗證。這些技術展現了 TUnit 的實用性,也體現了現代測試框架在實際專案應用中的價值。
執行控制最佳實踐:
整合測試策略:
複雜基礎設施編排:
Engine Modes 核心優勢:
TUnit 在以下方面具有顯著優勢:
但也需要考慮:
如果你正在考慮從 xUnit 遷移到 TUnit:
1. 評估階段:
2. 實施階段:
3. 驗證階段:
實用資源:
TUnit 代表了測試框架的發展方向:編譯時期最佳化和效能優先。特別是 Engine Modes 的設計概念,體現了現代軟體開發的幾個重要趨勢:
編譯時期最佳化趨勢:
技術演進趨勢:
隨著 .NET 生態系統的發展,特別是雲原生和微服務架構的普及,TUnit 的設計理念變得更加重要:
但無論選擇哪個測試框架,核心原則始終不變:寫出可讀、可維護、可靠的測試程式碼。技術只是工具,良好的測試設計思維才是根本。
明天我們將探討 Day 31 – 測試執行與診斷實戰:命令列技巧與除錯策略,包括:
我們將從實際開發場景出發,學習如何診斷和解決各種測試問題,確保測試套件的穩定性和可靠性。
雖然連續 30 天發文已經完成挑戰,但還是有些內容想分享給大家,所以明天繼續。
這是「重啟挑戰:老派軟體工程師的測試修練」的第三十天。明天會介紹 Day 31 – 測試執行與診斷實戰:命令列技巧與除錯策略。