上一章中我們完成了一個簡單的登入功能, 這一章主要示範如何對Stylet工程中的ViewModel進行單元測試.
回憶一下我們的登入邏輯,主要有以下4點:
- 當"使用者名"或"密碼"為空時, 是不允許登入的("登入"按鈕處于禁用狀态).
- 使用者名或密碼不正确時, 顯示"使用者名或密碼不正确"的消息框.
- 使用者名輸入"waku", 并且密碼輸入"123", 登入成功視窗關閉, 回到主視窗.
- 點選登入視窗右上角的"X"按鈕,整個應用程式退出.
那麼我們就嘗試編寫代碼來進行測試吧.
這裡我們隻測試ViewModel中的邏輯是否正确,對于UI測試則是另一個話題了,以後有機會再寫.
建立測試工程
VS2019支援三種測試架構: MSTest, Nunit和xUnit, 功能上差不多, 你可以選擇一個你喜歡的. 這裡我們使用xUnit.
建立一個名為
StyletBookStore.Test
的xUnit Test Project(.NET Core)工程:
然後對測試工程進行以下操作:
- 添加對
工程的引用, 這是我們測試的對象StyletBookStore
- 添加Moq包,我們使用Moq模拟一些Stylet的元件
Install-Package Moq -Version 4.13.1
- 添加Shouldly包,友善我們寫Assert代碼
Install-Package Shouldly -Version 3.0.2
在
StyletBookStore.Test
工程中建立一個名為
LoginViewModelTest
的類, 在其中編寫測試代碼.
-
配置Stylet的IoC容器
因為我們的
使用了依賴注入,是以在測試代碼中最好也是使用IoC來建立測試對象.在LoinViewModel
的構造方法中增加以下代碼:LoginViewModelTest
public LoginViewModelTest() { // 向Stylet的IoC中注冊服務 var builder = new StyletIoCBuilder(); builder.Bind<LoginViewModel>().ToSelf(); _container = builder.BuildContainer(); }
- Stylet的IoC容器需要使用
提供的API來建立, 是以首先我們建立了StyletIoCBuilder
的執行個體.StyletIoCBuilder
- 使用
範型方法注冊服務, 這裡我們将Bind<T>
LoginViewModel
的自身注冊進去.
更多關于Stylet的IoC配置方法請浏覽WIKI
- 最後使用
方法建立IoC容器, 由于我們需要在測試方法中使用該容器,是以需要定義一個成員變量來存儲它:BuildContainer
private readonly IContainer _container;
- Stylet的IoC容器需要使用
-
測試功能點: 當"使用者名"或"密碼"為空時, 是不允許登入的("登入"按鈕處于禁用狀态).
先增加一個測試方法, 用來測試密碼未輸入時, CanLogin應該傳回false:
/// <summary> /// 密碼未輸入, 不允許點選登入 /// </summary> [Fact] public void CanLoginTest_NoPassword() { // Arrange var vm = _container.Get<LoginViewModel>(); vm.UserName = "waku"; vm.Password = String.Empty; // Act bool canLogin = vm.CanLogin; // Assert canLogin.ShouldBe(false); }
- xUnit要求所有測試方法需要有
屬性.[Fact]
- 我們在測試方法中遵循AAA模式, 即Arrange, Act和Assert:
- Arrange: 設定測試對象并準備測試的先決條件
- Act: 執行測試的實際工作
- Assert: 驗證結果
- 使用Stylet的IoC容器取得
執行個體LoginViewModel
- 因為使用者名和密碼都是公有屬性, 是以我們直接通過代碼來修改它們.
- 使用Shouldly提供的擴充方法
來驗證ShouldBe
的值canLogin
- xUnit要求所有測試方法需要有
-
測試功能點: 使用者名或密碼不正确時, 顯示"使用者名或密碼不正确"的消息框.
因為登入邏輯中使用了
來顯示消息框, 這裡我們需要利用Moq來模拟它.在IWindowManager
構造方法中增加以下代碼:LoginViewModelTest
public LoginViewModelTest() { // 使用Moq虛拟IWindowManager _mockWindowManager = new Mock<IWindowManager>(); _mockWindowManager.Setup(_showMessageBoxExpr).Returns(MessageBoxResult.OK); ... builder.Bind<IWindowManager>().ToInstance(_mockWindowManager.Object); // 注冊IWindowManager ... }
-
來建立一個Mock對象,new Mock<T>
即是要Mock的實際類型. 後續我們需要使用Mock對象T
, 是以将其定義為一個成員變量:_mockWindowManager
private readonly Mock<IWindowManager> _mockWindowManager;
- 我們使用Moq的
方法來為指定的接口模拟一個方法, 該方法接收一個Expression類型的值. 為了簡潔性, 我們将Expression定義為一個成員變量:Setup
可以看出, 該Expression的定義和我們在private readonly Expression<Func<IWindowManager, MessageBoxResult>> _showMessageBoxExpr = wm => wm.ShowMessageBox("使用者名或密碼不正确", "登入失敗", MessageBoxButton.OK, MessageBoxImage.Exclamation, MessageBoxResult.None, MessageBoxResult.None, null, null, null);
方法中調用的形式是一緻的.Login
Moq的Expression不允許使用可選參數, 是以這裡我們将
的全部參數都明确寫出來.ShowMessageBox
關于Moq的詳細說明可浏覽這裡.
- 将模拟的
注冊進IoC容器中, 這裡使用了IWindowManager
來進行執行個體注冊. 通過Mock對象的ToInstance
屬性可以取得模拟對象.Object
/// <summary> /// 使用者名錯誤 /// </summary> [Fact] public void LoginTest_WrongUserName() { // Arrange var vm = _container.Get<LoginViewModel>(); vm.UserName = "wrong_username"; vm.Password = "123"; // Act vm.Login(); // Assert _mockWindowManager.Verify(_showMessageBoxExpr, Times.Once); // 應該顯示消息框 }
- 我們設定了一個錯誤的使用者名
.wrong_username
- 調用了
的LoginViewModel
方法.Login
- 使用Moq對象的
方法來驗證模拟方法被調用了.Verify
代表隻調用了一次, 如果未調用或調用次數不是一次,Times.Once
方法會抛出異常.Veryify
-
- 測試功能點: 使用者名輸入"waku", 并且密碼輸入"123", 點選"登入"按鈕, 登入視窗關閉, 回到主視窗.
方法中, 當驗證使用者名和密碼成功後, 我們使用了Login
RequestClose(true)
來請求關閉視窗. 我們怎麼來測試視窗關閉呢?
先看一下Stylet的
是如何實作的:RequestClose
/// <summary> /// Request that the conductor responsible for this screen close it /// </summary> /// <param name="dialogResult">DialogResult to return, if this is a dialog</param> public virtual void RequestClose(bool? dialogResult = null) { var conductor = this.Parent as IChildDelegate; if (conductor != null) { this.logger.Info("RequstClose called. Conductor: {0}; DialogResult: {1}", conductor, dialogResult); conductor.CloseItem(this, dialogResult); } else { var e = new InvalidOperationException(String.Format("Unable to close ViewModel {0} as it must have a conductor as a parent (note that windows and dialogs automatically have such a parent)", this.GetType())); this.logger.Error(e); throw e; } }
- 首先取得ViewModel的Parent, 這是一個實作了
的對象. 如未取到, 直接抛出異常.IChildDelegate
- 否則調用
方法, 将自身和視窗傳回值做為參數傳遞進去.IChildDelegate.CloseItem
- 使用Moq來模拟一個
對象.IChildDelegate
-
一個Setup
CloseItem(LoginViewModel, true)
- 将測試對象
的Parent設定為該模拟對象.LoginViewModel
類似:IWindowManager
測試方法:public class LoginViewModelTest { ... private readonly Mock<IWindowManager> _mockWindowManager; ... public LoginViewModelTest() { ... // 使用Moq虛拟IChildDelegate _mockChildDelegate = new Mock<IChildDelegate>(); ... builder.Bind<IChildDelegate>().ToInstance(_mockChildDelegate.Object); // 注冊IChildDelegate ... }
/// <summary> /// 正确的使用者名和密碼 /// </summary> [Fact] public void LoginTest() { // Arrange var vm = _container.Get<LoginViewModel>(); var childDelegate = _container.Get<IChildDelegate>(); vm.UserName = "waku"; vm.Password = "123"; vm.Parent = childDelegate; // Act vm.Login(); // Assert _mockWindowManager.Verify(_showMessageBoxExpr, Times.Never); // 不應該顯示消息框 _mockChildDelegate.Verify(cd => cd.CloseItem(vm, true), Times.Once); // 應該關閉視窗,并傳回true }
-
指定模拟的方法不應該被調用.(登入驗證成功, 不顯示消息框)Times.Never
- 驗證
被調用了一次.CloseItem(LoginViewModel, true)
我們隻需要驗證
被正确調用即可, 至于視窗是否能關閉那是Stylet需要確定的事了:)CloseItem
- 首先取得ViewModel的Parent, 這是一個實作了
-
測試功能點: 點選登入視窗右上角的"X"按鈕,整個應用程式退出.
首先我們回憶一下該功能的代碼是怎麼寫的:
protected override void OnViewLoaded() { var loginViewModel = _container.Get<LoginViewModel>(); var result = _windowManager.ShowDialog(loginViewModel); if (result != true) { RequestClose(); } }
- 該功能是在
ShellViewModel
方法中實作的,是以這是Shell中的功能, 是以我們需要建立一個新的測試類OnViewLoaded
, 來測試該功能.ShellViewModelTest
-
方法中同樣也使用了OnViewLoaded
, 和IWindowManager
方法, 是以那些Moq的東西也少不了.RequestClose
是一個protected方法, 我們不能在測試代碼中直接調用OnViewLoaded
ShellViewModel.OnViewLoaded
, 那麼該怎麼辦呢? 我們的Act該怎麼寫呢?
這裡介紹一個常用的技巧, 我們建立一個類繼承
的類, 定義一個public方法, 并在該方法中調用ShellViewModel
. 因為該類是ShellViewModel.OnViewLoaded
的子類, 是以ShellViewModel
的protected方法也可在子類中調用.代碼如下:ShellViewModel
至于其它的測試與Login中基本類似, 詳細的請看代碼./// <summary> /// 為了測試ShellViewModel.OnViewLoaded方法而建立的類 /// </summary> public class ShellViewModelForTest : ShellViewModel { public ShellViewModelForTest(IContainer container, IWindowManager windowManager) : base(container, windowManager) { } public void LoadView() { base.OnViewLoaded(); } }
- 該功能是在
至此, 我們的測試代碼就寫完了. 可以看出使用MVVM模式, 對于界面邏輯的測試是很簡單的. 這也是MVVM備受推崇的原因.
本篇到此為止, 希望朋友們能多多留言. 源碼托管在GITHUB上.
Happy Coding~