單元測試是確保代碼質量的核心手段,通過隔離測試目標代碼(System Under Test, SUT),驗證其邏輯正確性。結合Mock框架(如Moq)可以模擬外部依賴,使測試更可控、更高效。


1. xUnit/NUnit基礎:單元測試框架

1.1 xUnit與NUnit對比

特性

xUnit

NUnit

語法風格

屬性驅動([Fact][Theory]

屬性驅動([Test][TestCase]

並行測試

內置支持([Collection]

需顯式配置([Parallelizable]

數據驅動

[InlineData][MemberData]

[TestCase][TestCaseSource]

生命週期

類級共享(IClassFixture

靈活(SetUp/TearDown

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核心原則

  1. 紅-綠-重構循環
  • :編寫一個失敗的測試。
  • :編寫最少代碼使測試通過。
  • 重構:優化代碼結構,保持測試通過。
  1. 先寫測試,後寫實現:確保代碼可測試性。

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. 最佳實踐總結

  1. 測試命名規範
  • 方法名:MethodName_State_ExpectedResult(如Add_NegativeNumbers_ReturnsZero)。
  • 類名:ClassNameTests(如CalculatorTests)。
  1. Mock使用原則
  • 只Mock直接依賴(如IRepository),不Mock底層庫(如HttpClient)。
  • 避免過度Mock,保持測試與實際行為一致。
  1. TDD適用場景
  • 核心業務邏輯(如訂單處理、支付計算)。
  • 複雜算法(如排序、數據轉換)。
  1. 測試覆蓋率
  • 目標:核心路徑覆蓋率≥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,可以構建高可靠性的代碼,同時保持開發效率和代碼質量。