$npx -y skills add kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-autofixture-nsubstitute-integrationAutoFixture 與 NSubstitute 整合指南 - 實現自動模擬 (Auto-Mocking)。當需要自動建立 Mock 物件、簡化複雜相依性注入測試時使用。涵蓋 AutoNSubstituteDataAttribute、Frozen 機制、Greedy 建構策略。包含 IMapper (AutoMapper/Mapster) 等特殊相依性的客製化處理。 Make sure to use this skill whenever the user mentions AutoFixture with NSubstitute, auto-mock
| 1 | # AutoFixture + NSubstitute 自動模擬整合 |
| 2 | |
| 3 | ## 核心價值 |
| 4 | |
| 5 | - **減少樣板程式碼**:不需要手動為每個介面建立 `Substitute.For<T>()` |
| 6 | - **自動處理複雜相依圖**:AutoFixture 會自動解析並建立所需的物件 |
| 7 | - **提升測試維護性**:當建構函式變更時,測試程式碼通常不需要同步修改 |
| 8 | - **保持測試重點**:讓開發者專注於測試邏輯而非物件建立 |
| 9 | |
| 10 | --- |
| 11 | |
| 12 | ## 套件安裝與設定 |
| 13 | |
| 14 | ### 必要套件 |
| 15 | |
| 16 | ```bash |
| 17 | # 核心套件 |
| 18 | dotnet add package AutoFixture.AutoNSubstitute |
| 19 | |
| 20 | # 相關套件(如尚未安裝) |
| 21 | dotnet add package AutoFixture |
| 22 | dotnet add package AutoFixture.Xunit2 |
| 23 | dotnet add package NSubstitute |
| 24 | dotnet add package xunit |
| 25 | ``` |
| 26 | |
| 27 | ### NuGet 套件資訊 |
| 28 | |
| 29 | | 套件名稱 | 用途 | NuGet 連結 | |
| 30 | | ----------------------------- | ------------------------------- | ------------------------------------------------------------------------ | |
| 31 | | `AutoFixture.AutoNSubstitute` | AutoFixture 與 NSubstitute 整合 | [nuget.org](https://www.nuget.org/packages/AutoFixture.AutoNSubstitute/) | |
| 32 | | `AutoFixture.Xunit2` | xUnit 整合(AutoData 屬性) | [nuget.org](https://www.nuget.org/packages/AutoFixture.Xunit2/) | |
| 33 | | `NSubstitute` | 模擬框架 | [nuget.org](https://www.nuget.org/packages/NSubstitute/) | |
| 34 | |
| 35 | --- |
| 36 | |
| 37 | ## 核心概念 |
| 38 | |
| 39 | ### AutoNSubstituteCustomization 的作用 |
| 40 | |
| 41 | 當在 AutoFixture 中加入 `AutoNSubstituteCustomization` 時,它會自動: |
| 42 | |
| 43 | 1. **偵測介面類型**:當 AutoFixture 遇到介面或抽象類別時 |
| 44 | 2. **自動建立替身**:使用 NSubstitute 的 `Substitute.For<T>()` 建立 Mock 物件 |
| 45 | 3. **注入相依性**:將這些替身物件注入到需要的建構函式中 |
| 46 | 4. **保持實例一致性**:確保相同類型的替身在同一個測試中保持一致 |
| 47 | |
| 48 | ```csharp |
| 49 | using AutoFixture; |
| 50 | using AutoFixture.AutoNSubstitute; |
| 51 | |
| 52 | // 建立包含 AutoNSubstitute 功能的 Fixture |
| 53 | var fixture = new Fixture().Customize(new AutoNSubstituteCustomization()); |
| 54 | |
| 55 | // 自動建立服務和其相依性 |
| 56 | // MyService 的所有介面相依性都會自動變成 NSubstitute 的替身 |
| 57 | var service = fixture.Create<MyService>(); |
| 58 | ``` |
| 59 | |
| 60 | ### FrozenAttribute 凍結機制 |
| 61 | |
| 62 | `[Frozen]` 屬性用來控制測試中某個類型的實例: |
| 63 | |
| 64 | - 當參數被標註為 `[Frozen]` 時,AutoFixture 會建立這個類別的一個實例並**凍結**它 |
| 65 | - 後續在測試方法中都會使用同一個已凍結的實例 |
| 66 | - 這對於需要設定相依性行為然後驗證 SUT 的測試特別重要 |
| 67 | |
| 68 | ```csharp |
| 69 | [Theory] |
| 70 | [AutoData] |
| 71 | public async Task TestMethod( |
| 72 | [Frozen] IRepository repository, // 這個 repository 會被凍結 |
| 73 | MyService sut) // sut 會使用同一個 repository |
| 74 | { |
| 75 | // 設定凍結實例的行為 |
| 76 | repository.GetAsync(Arg.Any<int>()).Returns(someData); |
| 77 | |
| 78 | // SUT 內部使用的是同一個 repository 實例 |
| 79 | var result = await sut.DoSomething(); |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | ### 參數順序的重要性 |
| 84 | |
| 85 | 使用 `[Frozen]` 時,**參數順序非常重要**: |
| 86 | |
| 87 | ```csharp |
| 88 | // 正確:Frozen 參數在 SUT 之前 |
| 89 | public async Task TestMethod( |
| 90 | [Frozen] IRepository repository, |
| 91 | MyService sut) |
| 92 | |
| 93 | // 錯誤:SUT 會使用不同的 repository 實例 |
| 94 | public async Task TestMethod( |
| 95 | MyService sut, |
| 96 | [Frozen] IRepository repository) // 太晚凍結了 |
| 97 | ``` |
| 98 | |
| 99 | --- |
| 100 | |
| 101 | ## 傳統方式 vs AutoNSubstitute 方式 |
| 102 | |
| 103 | ### 傳統手動方式 |
| 104 | |
| 105 | ```csharp |
| 106 | [Fact] |
| 107 | public async Task TraditionalWay() |
| 108 | { |
| 109 | // Arrange - 手動建立每個相依性 |
| 110 | var repository = Substitute.For<IRepository>(); |
| 111 | var logger = Substitute.For<ILogger<OrderService>>(); |
| 112 | var notificationService = Substitute.For<INotificationService>(); |
| 113 | var cacheService = Substitute.For<ICacheService>(); |
| 114 | |
| 115 | var sut = new OrderService(repository, logger, notificationService, cacheService); |
| 116 | |
| 117 | // 設定替身行為 |
| 118 | repository.GetOrderAsync(Arg.Any<int>()).Returns(someOrder); |
| 119 | |
| 120 | // Act |
| 121 | var result = await sut.GetOrderAsync(orderId); |
| 122 | |
| 123 | // Assert |
| 124 | result.Should().NotBeNull(); |
| 125 | } |
| 126 | ``` |
| 127 | |
| 128 | **問題**: |
| 129 | |
| 130 | - 當服務增加新相依性時,所有測試都需要修改 |
| 131 | - 大量重複的 `Substitute.For<T>()` 呼叫 |
| 132 | - 測試程式碼冗長,難以快速理解測試意圖 |
| 133 | |
| 134 | ### 使用 AutoNSubstitute 方式 |
| 135 | |
| 136 | ```csharp |
| 137 | [Theory] |
| 138 | [AutoDataWithCustomization] |
| 139 | public async Task WithAutoNSubstitute( |
| 140 | [Frozen] IRepository repository, |
| 141 | OrderService sut) |
| 142 | { |
| 143 | // Arrange - 相依性已自動建立,只需設定需要的行為 |
| 144 | repository.GetOrderAsync(Arg.Any<int>()).Returns(someOrder); |
| 145 | |
| 146 | // Act |
| 147 | var result = await sut.GetOrderAsync(orderId); |
| 148 | |
| 149 | // Assert |
| 150 | result.Should().NotBeNull(); |
| 151 | } |
| 152 | ``` |
| 153 | |
| 154 | **優勢**: |
| 155 | |
| 156 | - 只需宣告需要互動的相依性 |
| 157 | - 其他相依性(logger, notificationService, cacheService)自動建立 |
| 158 | - 建構函式變更時,測試通常不需要修改 |
| 159 | |
| 160 | --- |
| 161 | |
| 162 | ## 自訂 AutoData 屬性 |
| 163 | |
| 164 | ### 為什麼需要自訂 AutoData 屬性? |
| 165 | |
| 166 | 在實際專案中,通常需要整合多種客製化設定: |
| 167 | |
| 168 | - **AutoNSubstituteCustomization**:自動為介面建立 NSubstitute 替身 |
| 169 | - **專案特定的 Customization**:如 Mapper 設定、驗證器設定等 |
| 170 | - **一致的測試基礎設施**:確保整個專案使用相同的設定 |
| 171 | |
| 172 | ### AutoDataWithCustomizationAttribute 實作 |
| 173 | |
| 174 | ```csharp |
| 175 | using AutoFixture; |
| 176 | using AutoFixture.AutoNSubstitute; |
| 177 | using AutoFixture.Xunit2; |
| 178 | |
| 179 | namespace MyProject.Tests.AutoFixtureConfigurations; |
| 180 | |
| 181 | /// <summary> |
| 182 | /// 包含客製化設定的 AutoData 屬性 |
| 183 | /// </summary> |
| 184 | public class AutoDataWithCustomiz |