單元測試是確保代碼質量的核心手段,通過隔離測試目標代碼(System Under Test, SUT),驗證其邏輯正確性。結合Mock框架(如Moq)可以模擬外部依賴,使測試更可控、更高效。
1. xUnit/NUnit基礎:單元測試框架
1.1 xUnit與NUnit對比
|
特性 |
xUnit |
NUnit |
|
語法風格 |
屬性驅動( |
屬性驅動( |
|
並行測試 |
內置支持( |
需顯式配置( |
|
數據驅動 |
|
|
|
生命週期 |
類級共享( |
靈活( |
1.2 xUnit基礎示例
csharp
// 簡單測試(Fact)
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
}
// 數據驅動測試(Theory + InlineData)
[Theory]
[InlineData(1, 1, 2)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
public void Add_MultipleInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
}
1.3 NUnit基礎示例
csharp
public class CalculatorTests
{
[SetUp] // 每個測試前執行
public void Setup() { /* 初始化代碼 */ }
[Test]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
Assert.AreEqual(5, calculator.Add(2, 3));
}
[TestCase(1, 1, 2)]
[TestCase(-1, 1, 0)]
public void Add_MultipleInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var calculator = new Calculator();
Assert.AreEqual(expected, calculator.Add(a, b));
}
}
2. Moq框架使用:模擬依賴對象
2.1 Moq核心功能
- 模擬接口/虛方法:通過
Mock<T>創建模擬對象。 - 驗證調用:檢查方法是否被調用、調用次數、參數等。
- 設置返回值/異常:模擬依賴對象的行為。
2.2 基礎用法
示例1:模擬接口並設置返回值
csharp
public interface ILogger
{
void Log(string message);
}
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger) => _logger = logger;
public void PlaceOrder(string orderId)
{
_logger.Log($"Order {orderId} placed.");
// 其他邏輯...
}
}
// 測試代碼
[Fact]
public void PlaceOrder_CallsLogger()
{
// 創建Mock對象
var mockLogger = new Mock<ILogger>();
var orderService = new OrderService(mockLogger.Object);
// 執行測試
orderService.PlaceOrder("ORD123");
// 驗證Logger.Log被調用一次,且參數為"Order ORD123 placed."
mockLogger.Verify(l => l.Log("Order ORD123 placed."), Times.Once);
}
示例2:模擬依賴並返回預設值
csharp
public interface IRepository
{
Product GetById(int id);
}
public class ProductService
{
private readonly IRepository _repo;
public ProductService(IRepository repo) => _repo = repo;
public decimal GetProductPrice(int id) => _repo.GetById(id).Price;
}
// 測試代碼
[Fact]
public void GetProductPrice_ReturnsCorrectPrice()
{
// 模擬Repository返回預設Product
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(r => r.GetById(1)).Returns(new Product { Id = 1, Price = 100m });
var service = new ProductService(mockRepo.Object);
var price = service.GetProductPrice(1);
Assert.Equal(100m, price);
}
2.3 高級用法
驗證方法調用次數
csharp
[Fact]
public void PlaceOrder_LogsExactlyOnce()
{
var mockLogger = new Mock<ILogger>();
var service = new OrderService(mockLogger.Object);
service.PlaceOrder("ORD123");
// 驗證Log被調用1次
mockLogger.Verify(l => l.Log(It.IsAny<string>()), Times.Once);
}
模擬拋出異常
csharp
[Fact]
public void GetProductPrice_ThrowsWhenProductNotFound()
{
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(r => r.GetById(999)).Throws<InvalidOperationException>();
var service = new ProductService(mockRepo.Object);
Assert.Throws<InvalidOperationException>(() => service.GetProductPrice(999));
}
3. 測試驅動開發(TDD)流程
3.1 TDD核心原則
- 紅-綠-重構循環:
- 紅:編寫一個失敗的測試。
- 綠:編寫最少代碼使測試通過。
- 重構:優化代碼結構,保持測試通過。
- 先寫測試,後寫實現:確保代碼可測試性。
3.2 TDD示例:實現一個棧(Stack)
步驟1:編寫失敗測試
csharp
public class StackTests
{
[Fact]
public void Push_AddsItemToStack()
{
var stack = new Stack<int>();
stack.Push(1);
Assert.Equal(1, stack.Pop());
}
}
結果:編譯失敗(
Stack類未實現)。
步驟2:實現最小代碼
csharp
public class Stack<T>
{
private List<T> _items = new List<T>();
public void Push(T item) => _items.Add(item);
public T Pop() => _items.RemoveAt(_items.Count - 1);
}
結果:測試通過。
步驟3:重構並添加新測試
csharp
[Fact]
public void Pop_ThrowsWhenStackIsEmpty()
{
var stack = new Stack<int>();
Assert.Throws<InvalidOperationException>(() => stack.Pop());
}
重構:修改
Pop()方法,添加空棧檢查。
csharp
public T Pop()
{
if (_items.Count == 0) throw new InvalidOperationException("Stack is empty.");
return _items.RemoveAt(_items.Count - 1);
}
3.3 TDD的優勢
- 減少缺陷:通過測試驅動設計,提前發現邊界條件。
- 提高可維護性:代碼結構更清晰,依賴更明確。
- 促進重構:有測試保障,可以安全優化代碼。
4. 最佳實踐總結
- 測試命名規範:
- 方法名:
MethodName_State_ExpectedResult(如Add_NegativeNumbers_ReturnsZero)。 - 類名:
ClassNameTests(如CalculatorTests)。
- Mock使用原則:
- 只Mock直接依賴(如
IRepository),不Mock底層庫(如HttpClient)。 - 避免過度Mock,保持測試與實際行為一致。
- TDD適用場景:
- 核心業務邏輯(如訂單處理、支付計算)。
- 複雜算法(如排序、數據轉換)。
- 測試覆蓋率:
- 目標:核心路徑覆蓋率≥90%,邊緣條件單獨測試。
- 工具:使用
dotnet test --collect:"XPlat Code Coverage"生成報告。
示例:完整TDD+Mock測試
csharp
public class OrderProcessorTests
{
[Fact]
public void ProcessOrder_ValidOrder_SendsEmailAndUpdatesInventory()
{
// 模擬依賴
var mockEmailService = new Mock<IEmailService>();
var mockInventory = new Mock<IInventoryService>();
mockInventory.Setup(i => i.UpdateStock(It.IsAny<int>(), It.IsAny<int>())).Returns(true);
// 創建SUT
var processor = new OrderProcessor(mockEmailService.Object, mockInventory.Object);
var order = new Order { Id = 1, ProductId = 100, Quantity = 2 };
// 執行測試
processor.ProcessOrder(order);
// 驗證
mockEmailService.Verify(e => e.Send(order.Id, "Order confirmed"), Times.Once);
mockInventory.Verify(i => i.UpdateStock(order.ProductId, -order.Quantity), Times.Once);
}
}
通過結合xUnit/NUnit、Moq和TDD,可以構建高可靠性的代碼,同時保持開發效率和代碼質量。