測試驅動開發(TDD, Test-Driven Development)由 Kent Beck 推廣,核心循環為 紅-綠-重構。本文說明 TDD 核心概念、與傳統開發差異,並示範在 ASP.NET MVC 專案中如何落地實施。
| 模式 | 流程 | 特點 |
|---|---|---|
| Code → Bug Fix | 寫碼 → 發現 Bug → 修補 | 無測試、品質不可控、維護成本高 |
| Code → Unit Test → Bug Fix | 寫碼 → 寫測試 → 執行測試 → 修補 | 有測試但為事後補充、覆蓋率難保證 |
| TDD | 寫測試 → 寫碼通過測試 → 重構 | 測試優先、設計導向、高覆蓋率、安全重構 |


需求:依分類 ID 查詢產品,支援分頁,若分類不存在拋出例外。
// 專案結構
MyApp.sln
├── MyApp.Web (MVC 專案)
├── MyApp.Core (領域模型、介面)
├── MyApp.Data (資料存取實作)
└── MyApp.Tests (單元測試專案 - 參考 xUnit/NUnit + Moq)
public class ProductServiceTests
{
private readonly Mock _mockRepo;
private readonly ProductService _service;
public ProductServiceTests()
{
_mockRepo = new Mock();
_service = new ProductService(_mockRepo.Object);
}
[Fact]
public void GetProductsByCategory_CategoryNotExists_ThrowsArgumentException()
{
// Arrange
_mockRepo.Setup(r => r.CategoryExists(999)).Returns(false);
// Act & Assert
Assert.Throws(() => _service.GetProductsByCategory(999, 1, 10));
}
}
執行測試 → 紅燈(尚未實作 ProductService)
public class ProductService : IProductService
{
private readonly IProductRepository _repo;
public ProductService(IProductRepository repo)
{
_repo = repo;
}
public PagedList GetProductsByCategory(int categoryId, int page, int pageSize)
{
if (!_repo.CategoryExists(categoryId))
throw new ArgumentException("分類不存在", nameof(categoryId));
return _repo.GetProductsByCategory(categoryId, page, pageSize);
}
}
執行測試 → 綠燈
[Fact]
public void GetProductsByCategory_ValidCategory_ReturnsPagedProducts()
{
// Arrange
var expectedProducts = new List { new Product { Id = 1, Name = "產品A" } };
_mockRepo.Setup(r => r.CategoryExists(1)).Returns(true);
_mockRepo.Setup(r => r.GetProductsByCategory(1, 1, 10))
.Returns(new PagedList(expectedProducts, 1, 10, 1));
// Act
var result = _service.GetProductsByCategory(1, 1, 10);
// Assert
Assert.Single(result);
Assert.Equal("產品A", result[0].Name);
}
public class ProductControllerTests
{
[Fact]
public void CategoryProducts_ValidId_ReturnsPartialViewWithModel()
{
// Arrange
var mockService = new Mock();
mockService.Setup(s => s.GetProductsByCategory(1, 1, 10))
.Returns(new PagedList(new[] { new Product { Name = "Test" } }, 1, 10, 1));
var controller = new ProductController(mockService.Object);
// Act
var result = controller.CategoryProducts(1, 1, 10) as PartialViewResult;
// Assert
Assert.NotNull(result);
Assert.Equal("_ProductList", result.ViewName);
var model = Assert.IsType>(result.Model);
Assert.Single(model);
}
}
| 層級 | 測試重點 | 工具建議 |
|---|---|---|
| 領域模型 | 商業規則、驗證邏輯、狀態轉換 | xUnit + FluentAssertions |
| 服務層 | 協調邏輯、異常處理、交易邊界 | Moq / NSubstitute 模擬 Repository |
| Controller | Action 結果型別、Model 綁定、導向、ViewName | MVC 測試專用 Helper (如 MvcContrib.TestHelper) |
| 路由 | URL 對應 Controller/Action、參數解析 | RouteDebugger / 自訂路由測試 |
| 過濾器 | 授權、例外處理、記錄行為 | 獨立測試 FilterAttribute 執行邏輯 |
Method_Scenario_ExpectedBehavior 命名慣例TDD 不僅是測試技術,更是軟體設計方法論。在 ASP.NET MVC 專案中導入 TDD,能強迫開發者從「使用者視角」設計 API、保持低耦合高內聚、建立安全重構網。雖初期投入較大,但長期維護成本顯著降低,是追求軟體品質團隊的必備實踐。