前面分享的兩篇文章,分別介紹了 Microsoft.Bcl.TimeProvider 和 AutoFixture 的 AutoData
另外在寫測試時,可以使用 FakeTimeProvider 去做抽換,就可以設定時區、時間來完成測試情境的執行。
那麼是不是也可以用 AutoData 的特性,讓我們在寫測試的時候可以省下建立 FakeTimeProvider 的步驟呢?當然可以,不過需要先瞭解該怎麼做、可以怎麼做以及應該如何做。
建立 FakeTimeProviderCustomization 類別
先建立這個類別,等一下就可以放進 AutoDataWithCustomization 裡。之後測試案例在建立 SUT (System Under Test) 測試目標物件的時後,就可以自動建立 FakeTimeProvider 並注入到 SUT 裡,並之後在測使方法裡也能夠在方法簽章裡透過 Frozen 取得。
public class FakeTimeProviderCustomization : ICustomization
{
/// <summary>
/// Customizes the fixture
/// </summary>
/// <param name="fixture">The fixture</param>
public void Customize(IFixture fixture)
{
fixture.Register(() => new FakeTimeProvider());
}
}
調整 AutoDataWithCustomizationAttribute 類別
在 AutoDataWithCustomizationAttribute 的 CreateFixture 方法裡增加 Customize 設定已加入 FakeTimeProviderCustomization
public class AutoDataWithCustomizationAttribute : AutoDataAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoDataWithCustomizationAttribute"/> class
/// </summary>
public AutoDataWithCustomizationAttribute() : base(CreateFixture)
{
}
private static IFixture CreateFixture()
{
var fixture = new Fixture().Customize(new AutoNSubstituteCustomization())
.Customize(new MapsterMapperCustomization())
.Customize(new FakeTimeProviderCustomization());
return fixture;
}
}
調整測試案例
先來看看第一個測試案例原本的樣子
[Fact]
public void IsTradeNow_取得的目前時間為假日_應回傳false()
{
// arrange
var currentTime = new DateTime(2024, 9, 28, 0, 0, 0, DateTimeKind.Local);
this._fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.Local);
this._fakeTimeProvider.SetUtcNow(TimeZoneInfo.ConvertTimeToUtc(currentTime));
this._holidayRepository.GetHolidays(Arg.Any<int>(), Arg.Any<int>()).Returns(Holidays);
// act
var actual = this._sut.IsTradeNow();
// assert
actual.Should().BeFalse();
}
將這個測試方法從使用 Fact 改為使用 Theory 與 AutoDataWithCustomization,並修改方法簽章,透過 Frozen 取得 stub 以及 SUT
[Theory]
[AutoDataWithCustomization]
public void IsTradeNow_取得的目前時間為假日_應回傳false(
[Frozen] FakeTimeProvider fakeTimeProvider,
[Frozen] IHolidayRepository holidayRepository,
TradeService sut)
{
// arrange
var currentTime = new DateTime(2024, 9, 28, 0, 0, 0, DateTimeKind.Local);
fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.Local);
fakeTimeProvider.SetUtcNow(TimeZoneInfo.ConvertTimeToUtc(currentTime));
holidayRepository.GetHolidays(Arg.Any<int>(), Arg.Any<int>()).Returns(Holidays);
// act
var actual = sut.IsTradeNow();
// assert
actual.Should().BeFalse();
}
看起來應該沒有什麼問題,我們就執行測試來驗證結果吧
實際執行後卻出現了狀況
由上圖裡可以看到 SUT 裡所使用的 TimeProvider 並不是我們所預期應該要使用 FakeTimeProvider,而是直接使用預設的 TimeProvider.SystemTimeProvider…
這也說明了我們所建立的 FakeTimeProviderCustomization 與修改 AutoDataWithCustomization,然後又在測試方法簽章裡使用 Frozen 取得 FakeTimeProvider 並在測試程式裡做設定,這一切都不管用。
認識 AutoFixture 的 Matching 列舉
FrozenAttribute 類別的建構式裡預設是使用了 Matching.ExactType,就是說當你在測試方法簽章使用 Frozen 並指定什麼型別後,就會給你什麼,然後 SUT 所相依注入又剛好是 Frozen 所凍結的確切型別就會拿來使用。
https://github.com/AutoFixture/AutoFixture/blob/master/Src/AutoFixture.xUnit2/FrozenAttribute.cs
回到我們所修改的測試方法,我們所指定凍結的是 FakeTimeProvider 型別的物件,但是 TradeService 建構式所要注入的是 TimeProvder 型別,而 FakeTimeProvider 雖然是繼承自 TimeProvider,但以 AutoData 的設定裡就是不會將 TimeProvider 去用 FakeTimeProvider 做替代。
而 FrozenAttribute 還有提供另外的建構式,是可以讓我們自己去指定 Matching
https://github.com/AutoFixture/AutoFixture/blob/master/Src/AutoFixture.xUnit2/Matching.cs
Matching 列舉的各個項目是代表什麼意思?以下是 ChatGPT 的說明
AutoFixture.Xunit2 中的 Matching 列舉允許開發者通過 FrozenAttribute 指定精確的凍結範圍。當我們希望凍結特定的類型實例時,可能需要進一步指定其凍結的匹配條件,如基於類型、基於接口或基於基類等。Matching 列舉的每個選項都對應特定的情境,根據它們,我們可以控制 FrozenAttribute 在測試中的具體行為。
以下是 Matching 列舉中各個選項的說明以及它們的典型使用情境:
1. ExactType
- 說明: 這個選項凍結類型與參數完全相同的實例。如果測試中的依賴項與 FrozenAttribute 修飾的參數具有相同的類型,則會使用已凍結的實例。
- 典型使用情境: 當你希望只針對具體類型進行凍結,並且不希望凍結其他基類或接口的情況下使用。
2. DirectBaseType
- 說明: 凍結與參數類型的直接基類相同的依賴項。如果測試中有一個依賴需要的類型是 FrozenAttribute 修飾參數的基類,則會使用凍結實例。
- 典型使用情境: 當你想測試繼承結構中的基類行為,並希望凍結基類的實例時使用。
3. ImplementedInterfaces
- 說明: 凍結類型實現的接口。當測試中的依賴需要的類型是某個接口,而這個接口由 FrozenAttribute 修飾的類型實現時,會使用凍結的實例。
- 典型使用情境: 當你測試依賴於接口的系統,並希望凍結接口的實現類,從而在整個測試中使用相同的實例。
4. ParameterName
- 說明: 凍結與構造函數或方法參數名稱相匹配的依賴項。當測試中的依賴與指定的參數名稱匹配時,會使用凍結的實例。
- 典型使用情境: 當你希望針對某個特定的參數進行凍結,而不希望凍結同類型的其他參數時,可以使用這個選項。
5. PropertyName
- 說明: 凍結與類型中屬性名稱相匹配的依賴項。當屬性名稱與測試中的依賴項匹配時,會使用凍結的實例。
- 典型使用情境: 當你希望只針對某個特定的屬性凍結依賴,而不凍結其他相同類型的屬性時使用。
6. FieldName
- 說明: 凍結與類型中欄位名稱相匹配的依賴項。當欄位名稱與測試中的依賴項匹配時,會使用凍結的實例。
- 典型使用情境: 當你有多個相同類型的欄位,但只希望凍結其中一個具體名稱的欄位時使用。
7. MemberName
- 說明: 凍結與成員名稱(包括參數名、屬性名和欄位名)相匹配的依賴項。這是 ParameterName、PropertyName 和 FieldName 的組合,會對這三種成員名稱進行匹配,並凍結符合名稱的依賴。
- 典型使用情境: 當你希望對所有參數、屬性和欄位名稱進行統一凍結時使用。
總結
- ExactType: 凍結與參數類型完全匹配的依賴項。
- DirectBaseType: 凍結與參數類型的直接基類相匹配的依賴項。
- ImplementedInterfaces: 凍結與參數類型實現的接口相匹配的依賴項。
- ParameterName: 凍結與構造函數或方法參數名稱相匹配的依賴項。
- PropertyName: 凍結與屬性名稱相匹配的依賴項。
- FieldName: 凍結與欄位名稱相匹配的依賴項。
- MemberName: 凍結與參數名、屬性名或欄位名相匹配的依賴項(ParameterName、PropertyName 和 FieldName 的組合)。
修改測試案例
由上面的 Matching 列舉的說明裡,我們得知可以使用 Matching.DirectBaseType 這個項目,因為 FakeTimeProvider 是繼承自 TimeProvider
於是修改了測試方法的程式
[Theory]
[AutoDataWithCustomization]
public void IsTradeNow_取得的目前時間為假日_應回傳false(
[Frozen(Matching.DirectBaseType)] FakeTimeProvider fakeTimeProvider,
[Frozen] IHolidayRepository holidayRepository,
TradeService sut)
{
// arrange
var currentTime = new DateTime(2024, 9, 28, 0, 0, 0, DateTimeKind.Local);
fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.Local);
fakeTimeProvider.SetUtcNow(TimeZoneInfo.ConvertTimeToUtc(currentTime));
holidayRepository.GetHolidays(Arg.Any<int>(), Arg.Any<int>()).Returns(Holidays);
// act
var actual = sut.IsTradeNow();
// assert
actual.Should().BeFalse();
}
觀察測試執行時的狀態
上圖可以看到 SUT 所注入使用的是 FakeTimeProvider
然後進入到實作裡再做確認,的確是注入使用 FakeTimeProvider
於是就這樣解決啦!
其他的解決方式
在尋找解決方法的過程時看到了某種方法,解法不見得比上面透過 Frozen 去指定 Matching.DirectBaseType 這樣簡單漂亮,但也讓我多認識另外的解法。
https://stackoverflow.com/questions/49576741/override-autofixture-customization-setup
上面在 stackoverflow 所找到的『Override Autofixture customization setup』為參考來源。
建立一個 AutoDataWithCustomizationType 類別,記得要繼承 AutoDataAttribute 類別
首先建立一個 TradeServiceCustomization 類別,需要繼承實作 ICustomization 介面
調整測試案例
這個解法也是可行,但是與直接在 FrozenAttribute 裡指定使用 Matching.DirectBaseType 的方式來比較的話,這個 with CustomizartonType 的方式就顯得繁瑣許多,不過至少比我以前在「單元測試使用 AutoFixture.AutoNSubstitute」這篇文章裡的土砲解法還要好很多了。
所以藉由我所提出的案例,讓大家多認識了 AutoFixture 的 Matching 列舉,以及在 Frozen 搭配 Matching 列舉的實際使用案例。
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力