共用方式為


新增單元測試

現在您的 ViewModel 和服務位於單獨的類別庫中,您可以輕鬆建立單元測試。 新增單元測試專案可讓您驗證 ViewModel 和服務是否如預期般運作,而不需要依賴 UI 層或手動測試。 您可以作為開發工作流程的一部分自動執行單元測試,確保您的程式碼保持可靠和可維護。

建立單元測試專案

  1. 以滑鼠右鍵按一下 [方案總管] 中的解決方案。
  2. 選取 [新增>專案...]。
  3. 選擇 WinUI 單元測試應用程式 範本,然後選取 [ 下一步]。
  4. 將專案命名為 WinUINotes.Tests,然後選取 [建立]。

新增專案參考

  1. 以滑鼠右鍵按一下 WinUINotes.Tests 專案,然後選取 [ 新增>專案參考...]。
  2. 檢查 WinUINotes.Bus 專案,然後選取 [確定]。

建立虛假的實作進行測試

為了進行測試,請建立檔案服務和儲存類別的虛假實作,這些實作實際上不會寫入磁碟。 Fakes 是輕量級實現,用於模擬真實依賴項的行為以進行測試。

  1. WinUINotes.Tests 專案中,建立名為 Fakes 的新資料夾。

  2. 在 Fakes 資料夾中新增類別檔案 FakeFileService.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Windows.Storage;
    using WinUINotes.Services;
    
    namespace WinUINotes.Tests.Fakes
    {
        internal class FakeFileService : IFileService
        {
            private Dictionary<string, string> fileStorage = [];
    
            public async Task CreateOrUpdateFileAsync(string filename, string contents)
            {
                if (fileStorage.ContainsKey(filename))
                {
                    fileStorage[filename] = contents;
                }
                else
                {
                    fileStorage.Add(filename, contents);
                }
    
                await Task.Delay(10); // Simulate some async work
            }
    
            public async Task DeleteFileAsync(string filename)
            {
                if (fileStorage.ContainsKey(filename))
                {
                    fileStorage.Remove(filename);
                }
    
                await Task.Delay(10); // Simulate some async work
            }
    
            public bool FileExists(string filename)
            {
                if (string.IsNullOrEmpty(filename))
                {
                    throw new ArgumentException("Filename cannot be null or empty", nameof(filename));
                }
    
                if (fileStorage.ContainsKey(filename))
                {
                    return true;
                }
    
                return false;
            }
    
            public IStorageFolder GetLocalFolder()
            {
                return new FakeStorageFolder(fileStorage);
            }
    
            public async Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync()
            {
                await Task.Delay(10);
                return GetStorageItemsInternal();
            }
    
            public async Task<IReadOnlyList<IStorageItem>> GetStorageItemsAsync(IStorageFolder storageFolder)
            {
                await Task.Delay(10);
                return GetStorageItemsInternal();
            }
    
            private IReadOnlyList<IStorageItem> GetStorageItemsInternal()
            {
                return fileStorage.Keys.Select(filename => CreateFakeStorageItem(filename)).ToList();
            }
    
            private IStorageItem CreateFakeStorageItem(string filename)
            {
                return new FakeStorageFile(filename);
            }
    
            public async Task<string> GetTextFromFileAsync(IStorageFile file)
            {
                await Task.Delay(10);
    
                if (fileStorage.ContainsKey(file.Name))
                {
                    return fileStorage[file.Name];
                }
    
                return string.Empty;
            }
        }
    }
    

    使用 FakeFileService 記憶體內字典 (fileStorage) 來模擬檔案操作,而無需接觸實際檔案系統。 主要功能包括:

    • 非同步模擬:用於 Task.Delay(10) 模擬真實的非同步文件操作
    • 驗證:針對無效輸入拋出例外,與真實實現相同
    • 與虛假儲存體類別整合:傳回可以一起運作以模擬 Windows 儲存體 API 的FakeStorageFolderFakeStorageFile實例
  3. 新增 FakeStorageFolder.cs:

    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices.WindowsRuntime;
    using Windows.Foundation;
    using Windows.Storage;
    using Windows.Storage.FileProperties;
    using Windows.Storage.Search;
    
    namespace WinUINotes.Tests.Fakes
    {
        internal class FakeStorageFolder : IStorageFolder
        {
            private string name;
            private Dictionary<string, string> fileStorage = [];
    
            public FakeStorageFolder(Dictionary<string, string> files)
            {
                fileStorage = files;
            }
    
            public FileAttributes Attributes => throw new NotImplementedException();
            public DateTimeOffset DateCreated => throw new NotImplementedException();
            public string Name => name;
            public string Path => throw new NotImplementedException();
    
            public IAsyncOperation<StorageFile> CreateFileAsync(string desiredName)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFile> CreateFileAsync(string desiredName, CreationCollisionOption options)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFolder> CreateFolderAsync(string desiredName)
            {
                throw new NotImplementedException();
            }
    
            // Only partial implementation shown for brevity
            ...
        }
    }
    

    在其 FakeStorageFolder 建構函式中採用檔案儲存字典,使其能夠使用與 FakeFileService相同的記憶體內檔案系統。 大部分的介面成員都會擲回 NotImplementedException ,因為只需要實作測試實際使用的屬性和方法。

    您可以在本教學課程的 FakeStorageFolder中檢視 的完整實作。

  4. 新增 FakeStorageFile.cs:

    using System;
    using System.IO;
    using System.Runtime.InteropServices.WindowsRuntime;
    using Windows.Foundation;
    using Windows.Storage;
    using Windows.Storage.FileProperties;
    using Windows.Storage.Streams;
    
    namespace WinUINotes.Tests.Fakes
    {
        public class FakeStorageFile : IStorageFile
        {
            private string name;
    
            public FakeStorageFile(string name)
            {
                this.name = name;
            }
    
            public string ContentType => throw new NotImplementedException();
            public string FileType => throw new NotImplementedException();
            public FileAttributes Attributes => throw new NotImplementedException();
            public DateTimeOffset DateCreated => throw new NotImplementedException();
            public string Name => name;
            public string Path => throw new NotImplementedException();
    
            public IAsyncOperation<StorageFile> CopyAsync(IStorageFolder destinationFolder)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFile> CopyAsync(IStorageFolder destinationFolder, string desiredNewName)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncOperation<StorageFile> CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option)
            {
                throw new NotImplementedException();
            }
    
            public IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace)
            {
                throw new NotImplementedException();
            }
    
            // Only partial implementation shown for brevity
            ...
        }
    }
    

    代表 FakeStorageFile 假儲存系統中的個別檔案。 它儲存檔案名稱並提供測試所需的最小實作。 就像 FakeStorageFolder一樣,它只實作正在測試的程式碼實際使用的成員。

    您可以在本教學課程的 FakeStorageFolder中檢視 的完整實作。

在檔中深入瞭解:

撰寫簡單的單元測試

  1. UnitTest1.cs 重新命名為 NoteTests.cs 並更新它:

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using System;
    using WinUINotes.Tests.Fakes;
    
    namespace WinUINotes.Tests
    {
        [TestClass]
        public partial class NoteTests
        {
            [TestMethod]
            public void TestCreateUnsavedNote()
            {
                var noteVm = new ViewModels.NoteViewModel(new FakeFileService());
                Assert.IsNotNull(noteVm);
                Assert.IsTrue(noteVm.Date > DateTime.Now.AddHours(-1));
                Assert.IsTrue(noteVm.Filename.EndsWith(".txt"));
                Assert.IsTrue(noteVm.Filename.StartsWith("notes"));
                noteVm.Text = "Sample Note";
                Assert.AreEqual("Sample Note", noteVm.Text);
                noteVm.SaveCommand.Execute(null);
                Assert.AreEqual("Sample Note", noteVm.Text);
            }
        }
    }
    

    此測試示範如何使用FakeFileService來對NoteViewModel進行單元測試。 測試會建立新的 NoteViewModel,檢查其初始狀態 (日期是最近的,檔名遵循預期的模式) ,在筆記上設定文字,執行儲存命令,並確認文字持續存在。 由於使用假檔案服務而不是真實實現,因此測試運行速度很快,無需任何實際的檔案 I/O,並且可以重複運行而不會產生副作用。

在檔中深入瞭解:

執行測試

  1. 在 Visual Studio 中開啟 [ 測試總管 ] 視窗 (測試>測試總管)。
  2. 選取 [執行所有測試 ] 以執行單元測試。
  3. 確認測試通過。

您現在擁有一個可測試的架構,可以在獨立於 UI 的情況下測試您的 ViewModels 和服務!

總結

在本教學課程系列中,您已瞭解如何:

  • 建立一個獨立的類別庫專案 (Bus project),用於存放 ViewModels 和服務,以便於將單元測試與 UI 層分開進行。
  • 使用 MVVM 工具組實作 MVVM 模式,利用 ObservableObject、 屬性 [ObservableProperty] ,並 [RelayCommand] 減少樣板程式碼。
  • 使用來源產生器自動建立屬性變更通知和命令實作。
  • 使用 [NotifyCanExecuteChangedFor] 當屬性值變更時自動更新指令的可用性。
  • 整合相依性注入,以 Microsoft.Extensions.DependencyInjection 管理 ViewModel 和服務的生命週期。
  • 建立介面 IFileService 和實作,以可測試的方式處理檔案作業。
  • App.xaml.cs 中設定 DI 容器,並從服務提供者擷取頁面中的 ViewModels。
  • 實作 以 WeakReferenceMessenger 啟用元件之間的鬆散耦合,讓網頁無需直接參照即可回應 ViewModel 事件。
  • 建立繼承的 ValueChangedMessage<T> 訊息類別,以便在元件之間攜帶資料。
  • 建立依賴項的虛假實作以進行測試,而不觸及實際的檔案系統。
  • 使用 MSTest 撰寫單元測試,以獨立於 UI 層驗證 ViewModel 行為。

此架構為建置可維護、可測試的 WinUI 應用程式提供堅實的基礎,並在 UI、商務邏輯和資料存取層之間明確區分關注點。 您可以從 GitHub 存放庫下載或檢視本教學課程的程式代碼。

後續步驟

現在您已瞭解如何使用 MVVM 工具組和相依性插入來實作 MVVM,您可以探索更進階的主題:

  • 進階訊息傳遞:探索其他訊息傳遞模式,包括請求/回應訊息和用於選擇性訊息處理的訊息令牌。
  • 驗證:使用資料註釋和 MVVM 工具組的驗證功能,將輸入驗證新增至 ViewModel。
  • 非同步命令:使用 AsyncRelayCommand進一步了解非同步命令執行、取消支援和進度報告。
  • 進階測試:探索更進階的測試場景,包括測試訊息處理、非同步命令執行和屬性變更通知。
  • 可觀察的集合:有效使用 ObservableCollection<T> 並探索 ObservableRangeCollection<T> 大量操作。