ViewModel とサービスが別のクラス ライブラリに配置されたので、単体テストを簡単に作成できます。 単体テスト プロジェクトを追加すると、UI レイヤーや手動テストに依存することなく、ViewModel とサービスが期待どおりに動作することを確認できます。 単体テストは開発ワークフローの一部として自動的に実行できるため、コードの信頼性と保守性を維持できます。
単体テスト プロジェクトを作成する
- ソリューション エクスプローラーでソリューションを右クリックします。
- [ 追加>新しいプロジェクト...] を選択します。
- WinUI 単体テスト アプリ テンプレートを選択し、[次へ] を選択します。
- プロジェクトに
WinUINotes.Tests名前を付け、[ 作成] を選択します。
プロジェクト参照を追加する
- WinUINotes.Tests プロジェクトを右クリックし、[追加>Project Reference...] を選択します。
- WinUINotes.Bus プロジェクトを確認し、[OK] を選択します。
テスト用の偽の実装を作成する
テスト用に、実際にディスクに書き込まないファイル サービスとストレージ クラスの偽の実装を作成します。 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 Storage API をシミュレートするために連携する
FakeStorageFolderインスタンスとFakeStorageFileインスタンスを返します
-
非同期シミュレーション:
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); } } }このテストでは、
NoteViewModelを使用してFakeFileServiceを単体テストする方法を示します。 テストでは、新しいNoteViewModelを作成し、その初期状態 (日付が最近、ファイル名が予期されるパターンに従う) を確認し、メモにテキストを設定し、保存コマンドを実行して、テキストが保持されていることを確認します。 実際の実装ではなく偽のファイル サービスが使用されるため、テストは実際のファイル I/O なしで迅速に実行され、副作用なしで繰り返し実行できます。
詳細については、次のドキュメントを参照してください。
テストの実行
- Visual Studio で [テスト エクスプローラー ] ウィンドウを開きます (Test>Test Explorer)。
- [ すべてのテストの実行 ] を選択して単体テストを実行します。
- テストに合格したことを確認します。
これで、UI とは別に ViewModel とサービスをテストできる、テスト可能なアーキテクチャが作成されました。
概要
このチュートリアル シリーズでは、次の方法を学習しました。
- ViewModel とサービスを保持する別のクラス ライブラリ プロジェクト (Bus プロジェクト) を作成し、UI レイヤーとは別の単体テストを有効にします。
- MVVM Toolkit を使用して MVVM パターンを実装し、
ObservableObject、[ObservableProperty]属性、および[RelayCommand]を利用して定型コードを削減します。 - ソース ジェネレーターを使用して、プロパティ変更通知とコマンド実装を自動的に作成します。
- プロパティ値が変更されたときにコマンドの可用性を自動的に更新するには、
[NotifyCanExecuteChangedFor]を使用します。 -
Microsoft.Extensions.DependencyInjectionを使用して依存関係の挿入を統合し、ViewModel とサービスのライフサイクルを管理します。 - テスト可能な方法でファイル操作を処理する
IFileServiceインターフェイスと実装を作成します。 -
App.xaml.csで DI コンテナーを構成し、ページ内のサービス プロバイダーから ViewModel を取得します。 -
WeakReferenceMessengerを実装してコンポーネント間の疎結合を有効にし、ページが直接参照なしで ViewModel イベントに応答できるようにします。 - コンポーネント間でデータを伝達するために、
ValueChangedMessage<T>から継承するメッセージ クラスを作成します。 - 実際のファイル システムに触れることなく、テスト用の依存関係の偽の実装を作成します。
- MSTest を使用して単体テストを記述し、UI レイヤーとは別に ViewModel の動作を確認します。
このアーキテクチャは、UI、ビジネス ロジック、およびデータ アクセス層間の懸念事項を明確に分離して、保守可能でテスト可能な WinUI アプリケーションを構築するための強固な基盤を提供します。 このチュートリアルのコードは 、GitHub リポジトリからダウンロードまたは表示できます。
次のステップ
MVVM ツールキットと依存関係の挿入を使用して MVVM を実装する方法を理解したら、より高度なトピックを調べることができます。
- 高度なメッセージング: 要求/応答メッセージ、選択的メッセージ処理用のメッセージ トークンなど、追加のメッセージング パターンについて説明します。
- 検証: データ注釈と MVVM Toolkit の検証機能を使用して、ViewModel に入力検証を追加します。
-
非同期コマンド:
AsyncRelayCommandを使用した非同期コマンドの実行、取り消しのサポート、進行状況レポートの詳細について説明します。 - 高度なテスト: メッセージ処理のテスト、非同期コマンドの実行、プロパティ変更通知など、より高度なテスト シナリオについて説明します。
-
監視可能なコレクション:
ObservableCollection<T>を効果的に使用し、一括操作のObservableRangeCollection<T>を調べます。
関連コンテンツ
Windows developer