現在您的 ViewModel 和服務位於單獨的類別庫中,您可以輕鬆建立單元測試。 新增單元測試專案可讓您驗證 ViewModel 和服務是否如預期般運作,而不需要依賴 UI 層或手動測試。 您可以作為開發工作流程的一部分自動執行單元測試,確保您的程式碼保持可靠和可維護。
建立單元測試專案
- 以滑鼠右鍵按一下 [方案總管] 中的解決方案。
- 選取 [新增>專案...]。
- 選擇 WinUI 單元測試應用程式 範本,然後選取 [ 下一步]。
- 將專案命名為
WinUINotes.Tests,然後選取 [建立]。
新增專案參考
- 以滑鼠右鍵按一下 WinUINotes.Tests 專案,然後選取 [ 新增>專案參考...]。
- 檢查 WinUINotes.Bus 專案,然後選取 [確定]。
建立虛假的實作進行測試
為了進行測試,請建立檔案服務和儲存類別的虛假實作,這些實作實際上不會寫入磁碟。 Fakes 是輕量級實現,用於模擬真實依賴項的行為以進行測試。
在 WinUINotes.Tests 專案中,建立名為 Fakes 的新資料夾。
在 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實例
-
非同步模擬:用於
新增
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,因為只需要實作測試實際使用的屬性和方法。新增
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一樣,它只實作正在測試的程式碼實際使用的成員。
在檔中深入瞭解:
撰寫簡單的單元測試
將
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,並且可以重複運行而不會產生副作用。
在檔中深入瞭解:
執行測試
- 在 Visual Studio 中開啟 [ 測試總管 ] 視窗 (測試>測試總管)。
- 選取 [執行所有測試 ] 以執行單元測試。
- 確認測試通過。
您現在擁有一個可測試的架構,可以在獨立於 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>大量操作。