單元測試

提示

本內容節錄自《Enterprise Application Patterns Using .NET MAUI》電子書,可以從 .NET Docs 取得,也可以免費下載 PDF 離線閱讀。

Enterprise Application Patterns Using .NET MAUI eBook cover thumbnail.

多平台應用程式會遭遇類似於桌面和 Web 應用程式的問題。 行動使用者會根據其裝置、網路連線能力、服務的可用性,以及各種其他因素而有所不同。 因此,因為多平台應用程式會用於真實世界,所以應該進行測試,以改善其品質、可靠性和效能。 許多類型的測試皆應在應用程式上執行,包括單元測試、整合測試和使用者介面測試。 單元測試是建置高品質應用程式最常見的形式和必要作業。

單元測試會採用應用程式的小型單位 (通常是方法),將其與程式碼的其餘部分隔離,並確認其行為正常運作。 其目標是檢查每個功能單位是否正常執行,使錯誤不至於傳播到整個應用程式內。 在發生錯誤的當下偵測 Bug,可以更有效率地間接觀察第二次失敗時 Bug 造成的影響。

當單元測試是軟體發展工作流程不可或缺的一部分時,對程式碼品質造成的影響最大。 單元測試可作為應用程式的設計文件和功能規格。 一旦方法撰寫完畢,就應該撰寫單元測試,以驗證回應標準、界限和不正確輸入資料案例的方法行為,並檢查程式碼所做的任何明確或隱含假設。 或者,在測試驅動開發中,也可以在程式碼之前撰寫單元測試。 如需測試驅動開發以及如何實作的詳細資訊,請參閱逐步解說:使用測試總管進行測試驅動開發。

注意

單元測試可有效解決迴歸。 也就是說,功能在過去運作正常,但卻因錯誤更新而受到干擾。

單元測試通常會使用 arrange-act-assert 模式:

步驟 描述
排列 會初始化物件,並為傳遞至受測方法的資料設定值。
採取行動 會使用必要的引數叫用受測方法。
Assert 驗證受測方法的動作是否正常。

此模式可確保單元測試是可讀取、自我描述和一致的。

相依性插入和單元測試

採用鬆散耦合架構的其中一項原因,是其有助於單元測試。 註冊至相依性插入服務的其中一種類型為 IAppEnvironmentService 介面。 下列程式碼範例顯示此類別的大綱:

public class OrderDetailViewModel : ViewModelBase
{
    private IAppEnvironmentService _appEnvironmentService;

    public OrderDetailViewModel(
        IAppEnvironmentService appEnvironmentService,
        IDialogService dialogService, INavigationService navigationService, ISettingsService settingsService)
        : base(dialogService, navigationService, settingsService)
    {
        _appEnvironmentService = appEnvironmentService;
    }
}

OrderDetailViewModel 類別具有 IAppEnvironmentService 類型的相依性,而相依性插入容器會在具現化 OrderDetailViewModel 物件時解析。 不過,這不會建立利用實際伺服器、裝置和組態來對 OrderDetailViewModel 類別進行單元測試的 IAppEnvironmentService 物件,而是將 IAppEnvironmentService 物件取代為模擬物件,以供測試之用。 模擬物件是具有相同物件或介面特徵標記的模擬物件,但是以特定方式建立,以協助進行單元測試。 它通常與相依性插入搭配使用來提供介面的特定實作,以測試不同的資料和工作流程案例。

此方法可讓 IAppEnvironmentService 物件在執行階段傳遞至 OrderDetailViewModel 類別,且出於可測試性考量,其可讓模擬類別在測試期間傳遞至 OrderDetailViewModel 類別。 這種方法的主要優點在於,其可在不需要龐大資源的情況下執行單元測試,例如執行階段平台功能、Web 服務或資料庫。

測試 MVVM 應用程式

從 MVVM 應用程式測試模型和檢視模型,與測試任何其他類別的方式相同,且會使用相同的工具和技術;這包括單元測試和模擬等功能。 不過,一些典型的模型和檢視模型類別模式,可以受益於特定的單元測試技術。

提示

請使用每個單元測試來測試一個項目。 測試的複雜度擴大,會使該測試的驗證更加困難。 藉由將單元測試限制為單一考量,我們可以確保測試會更容易重複、受到隔離,且執行時間較短。 如需更多最佳做法,請參閱使用 .NET Core 和 .NET Standard 的單元測試最佳做法

請勿針對多個層面的單元行為進行單元測試練習。 這樣做會產生難以閱讀和更新的測試。 在解譯失敗時,也可能會導致混淆。

eShopOnContainers 多平台應用程式會使用 xUnit 來執行單元測試,其支援兩種不同類型的單元測試:

測試類型 屬性 描述
事實 Fact 一律為 true 的測試,其會測試非變異的條件。
理論 Theory 僅特定資料集才為 true 的測試。

eShopOnContainers 多平台應用程式隨附的單元測試為事實測試,因此每個單元測試方法都會以 Fact 屬性裝飾。

測試非同步功能

實作 MVVM 模式時,檢視模型通常會以非同步方式叫用服務上的作業。 測試叫用這些作業的程式碼時,通常會使用模擬來取代實際服務。 下列程式碼範例示範如何將模擬服務傳遞至檢視模型來測試非同步功能:

[Fact]
public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()
{
    // Arrange
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    // Act
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    // Assert
    Assert.NotNull(orderViewModel.Order);
}

此單元測試會檢查 OrderDetailViewModel 執行個體的 Order 屬性在叫用 InitializeAsync 方法之後是否會有值。 當檢視模型的對應檢視瀏覽至 InitializeAsync 方法時,便會叫用該方法。 如需瀏覽的詳細資訊,請參閱瀏覽

建立 OrderDetailViewModel 執行個體時,它會預期 IOrderService 執行個體會指定為引數。 不過,OrderService 會從 Web 服務擷取資料。 因此,OrderMockService 執行個體 (OrderService 類別的模擬版本) 會指定為 OrderDetailViewModel 建構函式的引數。 然後,在叫用檢視模型的 InitializeAsync 方法 (使用 IOrderService 作業) 時,會擷取模擬資料,而不是與 Web 服務通訊。

測試 INotifyPropertyChanged 實作

實作 INotifyPropertyChanged 介面可讓檢視回應源自檢視模型和模型的變更。 這些變更不限於控制項中顯示的資料,它們也會用來控制檢視,例如導致動畫啟動或停用控制項的檢視模型狀態。

您可以藉由將事件處理常式附加至 PropertyChanged 事件,以及檢查事件是否在設定屬性的新值之後引發事件,以測試單元測試可以直接更新的屬性。 下列程式碼範例會示範這類測試:

[Fact]
public async Task SettingOrderPropertyShouldRaisePropertyChanged()
{
    var invoked = false;
    var orderService = new OrderMockService();
    var orderViewModel = new OrderDetailViewModel(orderService);

    orderViewModel.PropertyChanged += (sender, e) =>
    {
        if (e.PropertyName.Equals("Order"))
            invoked = true;
    };
    var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
    await orderViewModel.InitializeAsync(order);

    Assert.True(invoked);
}

此單元測試會叫用 OrderViewModel 類別的 InitializeAsync 方法,這會導致更新其 Order 屬性。 若針對 Order 屬性引發 PropertyChanged 事件,則將會通過單元測試。

測試訊息式通訊

檢視使用 MessagingCenter 類別在鬆散耦合類別之間通訊的模型,可藉由訂閱受測程式碼所傳送的訊息來進行單元測試,如下列程式碼範例所示:

[Fact]
public void AddCatalogItemCommandSendsAddProductMessageTest()
{
    var messageReceived = false;
    var catalogService = new CatalogMockService();
    var catalogViewModel = new CatalogViewModel(catalogService);

    MessagingCenter.Subscribe<CatalogViewModel, CatalogItem>(
        this, MessageKeys.AddProduct, (sender, arg) =>
    {
        messageReceived = true;
    });
    catalogViewModel.AddCatalogItemCommand.Execute(null);

    Assert.True(messageReceived);
}

本單元測試會檢查 CatalogViewModel 是否發佈 AddProduct 訊息,以作為其執行 AddCatalogItemCommand 的回應。 因為 MessagingCenter 類別支援多點傳送訊息訂閱,所以單元測試可以訂閱 AddProduct 訊息,並執行回呼委派作為回應來接收訊息。 此回呼委派 (指定為 Lambda 運算式) 會設定 Assert 陳述式用來驗證測試行為的布林值欄位。

測試例外狀況處理

您也可以撰寫單元測試來檢查是否會針對不正確動作或輸入擲回特定例外狀況,如下列程式碼範例所示:

[Fact]
public void InvalidEventNameShouldThrowArgumentExceptionText()
{
    var behavior = new MockEventToCommandBehavior
    {
        EventName = "OnItemTapped"
    };
    var listView = new ListView();

    Assert.Throws<ArgumentException>(() => listView.Behaviors.Add(behavior));
}

因為 ListView 控制項沒有名為 OnItemTapped 的事件,所以此單元測試會擲回例外狀況,。 Assert.Throws<T> 方法是泛型方法,其中 T 是預期例外狀況的類型。 傳遞至 Assert.Throws<T> 方法的引數,是會擲回例外狀況的 Lambda 運算式。 因此,如果 Lambda 運算式擲回 ArgumentException,則將會通過單元測試。

提示

避免撰寫檢查例外狀況訊息字串的單元測試。 例外狀況訊息字串可能會隨著時間而變更,因此依賴其存在的單元測試會被視為不盡完善。

測試驗證

測試驗證實作有兩個層面:測試任何驗證規則都已正確實作,並測試 ValidatableObject<T> 類別如預期地執行。

因為驗證邏輯通常為輸出相依於輸入的獨立式處理序,所以通常很容易測試。 您應該測試在至少有一個相關聯驗證規則之每個屬性上叫用 Validate 方法的結果,如下列程式碼範例所示:

[Fact]
public void CheckValidationPassesWhenBothPropertiesHaveDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";
    mockViewModel.Surname.Value = "Smith";

    var isValid = mockViewModel.Validate();

    Assert.True(isValid);
}

MockViewModel 執行個體中的兩個 ValidatableObject<T> 屬性都有資料時,此單元測試會檢查驗證是否成功。

除了檢查驗證是否成功,驗證單元測試也應該檢查每個 ValidatableObject<T> 執行個體其 ValueIsValidErrors 屬性的值,以確認類別是否正常執行。 下列程式碼範例示範執行這項作業的單元測試:

[Fact]
public void CheckValidationFailsWhenOnlyForenameHasDataTest()
{
    var mockViewModel = new MockViewModel();
    mockViewModel.Forename.Value = "John";

    bool isValid = mockViewModel.Validate();

    Assert.False(isValid);
    Assert.NotNull(mockViewModel.Forename.Value);
    Assert.Null(mockViewModel.Surname.Value);
    Assert.True(mockViewModel.Forename.IsValid);
    Assert.False(mockViewModel.Surname.IsValid);
    Assert.Empty(mockViewModel.Forename.Errors);
    Assert.NotEmpty(mockViewModel.Surname.Errors);
}

此單元測試會檢查當 MockViewModelSurname 屬性沒有任何資料,且每個 ValidatableObject<T> 執行個體的 ValueIsValidErrors 屬性都已正確設定時,驗證會失敗。

摘要

單元測試會採用應用程式的小型單位 (通常是方法),將其與程式碼的其餘部分隔離,並確認其行為符合預期。 其目標是檢查每個功能單位是否正常執行,使錯誤不至於傳播到整個應用程式。

測試中物件的行為可以隔離,方法是將相依物件取代為模擬相依物件行為的模擬物件。 這可在不需要龐大資源的情況下執行單元測試,例如執行階段平台功能、Web 服務或資料庫

從 MVVM 應用程式測試模型和檢視模型,與測試任何其他類別的方式相同,且可以使用相同的工具和技術。