【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(2)
上一章《
回憶一下我們的登錄邏輯,主要有以下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容器
因為我們的LoinViewModel使用了依賴注入,所以在測試代碼中最好也是使用IoC來創建測試對象.在LoginViewModelTest的構造方法中增加以下代碼:
public LoginViewModelTest() {// 向Stylet的IoC中注冊服務var builder = new StyletIoCBuilder();builder.Bind<LoginViewModel>().ToSelf();_container = builder.BuildContainer(); }Stylet的IoC容器需要使用StyletIoCBuilder提供的API來創建, 所以首先我們創建了StyletIoCBuilder的實例.
使用Bind<T>范型方法注冊服務, 這里我們將LoginViewModel的自身注冊進去.
更多關于Stylet的IoC配置方法請瀏覽WIKI
最后使用BuildContainer方法創建IoC容器, 由于我們需要在測試方法中使用該容器,所以需要定義一個成員變量來存儲它:
private readonly IContainer _container;
測試功能點: 當"用戶名"或"密碼"為空時, 是不允許登錄的("登錄"按鈕處于禁用狀態).
先增加一個測試方法, 用來測試密碼未輸入時, CanLogin應該返回false:
/// <summary> /// 密碼未輸入, 不允許點擊登錄 /// </summary> [Fact] public void CanLoginTest_NoPassword() {// Arrangevar vm = _container.Get<LoginViewModel>();vm.UserName = "waku";vm.Password = String.Empty;// Actbool canLogin = vm.CanLogin;// AssertcanLogin.ShouldBe(false); }測試"用戶名未輸入"和"用戶名和密碼都輸入"的代碼類似, 這里就不再詳細說明了, 可直接看代碼.
Arrange: 設置測試對象并準備測試的先決條件
Act: 執行測試的實際工作
Assert: 驗證結果
xUnit要求所有測試方法需要有[Fact]屬性.
我們在測試方法中遵循AAA模式, 即Arrange, Act和Assert:
使用Stylet的IoC容器取得LoginViewModel實例
因為用戶名和密碼都是公有屬性, 所以我們直接通過代碼來修改它們.
使用Shouldly提供的擴展方法ShouldBe來驗證canLogin的值
測試功能點: 用戶名或密碼不正確時, 顯示"用戶名或密碼不正確"的消息框.
因為登錄邏輯中使用了IWindowManager來顯示消息框, 這里我們需要利用Moq來模擬它.在LoginViewModelTest構造方法中增加以下代碼:
public LoginViewModelTest() {// 使用Moq虛擬IWindowManager_mockWindowManager = new Mock<IWindowManager>();_mockWindowManager.Setup(_showMessageBoxExpr).Returns(MessageBoxResult.OK);...builder.Bind<IWindowManager>().ToInstance(_mockWindowManager.Object); // 注冊IWindowManager... }有了Mock對象, 我們就可以來編寫驗證登錄邏輯的測試代碼了:
/// <summary> /// 用戶名錯誤 /// </summary> [Fact] public void LoginTest_WrongUserName() {// Arrangevar vm = _container.Get<LoginViewModel>();vm.UserName = "wrong_username";vm.Password = "123";// Actvm.Login();// Assert_mockWindowManager.Verify(_showMessageBoxExpr, Times.Once); // 應該顯示消息框 }還需要測試用戶名正確但是密碼不正確的情形, 就不詳細說明了.
我們設置了一個錯誤的用戶名wrong_username.
調用了LoginViewModel的Login方法.
使用Moq對象的Verify方法來驗證模擬方法被調用了.?Times.Once代表只調用了一次, 如果未調用或調用次數不是一次,?Veryify方法會拋出異常.
使用new Mock<T>來創建一個Mock對象,?T即是要Mock的實際類型. 后續我們需要使用Mock對象_mockWindowManager, 所以將其定義為一個成員變量:
private readonly Mock<IWindowManager> _mockWindowManager;我們使用Moq的Setup方法來為指定的接口模擬一個方法, 該方法接收一個Expression類型的值. 為了簡潔性, 我們將Expression定義為一個成員變量:
private readonly Expression<Func<IWindowManager, MessageBoxResult>> _showMessageBoxExpr = wm => wm.ShowMessageBox("用戶名或密碼不正確", "登錄失敗", MessageBoxButton.OK, MessageBoxImage.Exclamation, MessageBoxResult.None, MessageBoxResult.None, null, null, null);可以看出, 該Expression的定義和我們在Login方法中調用的形式是一致的.
Moq的Expression不允許使用可選參數, 所以這里我們將ShowMessageBox的全部參數都明確寫出來.
關于Moq的詳細說明可瀏覽這里.
將模擬的IWindowManager注冊進IoC容器中, 這里使用了ToInstance來進行實例注冊. 通過Mock對象的Object屬性可以取得模擬對象.
測試功能點: 用戶名輸入"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;} }所以解決方案就出來了:
Mock相關的代碼如下, 與MockIWindowManager類似:
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() {// Arrangevar vm = _container.Get<LoginViewModel>();var childDelegate = _container.Get<IChildDelegate>();vm.UserName = "waku";vm.Password = "123";vm.Parent = childDelegate;// Actvm.Login();// Assert_mockWindowManager.Verify(_showMessageBoxExpr, Times.Never); // 不應該顯示消息框_mockChildDelegate.Verify(cd => cd.CloseItem(vm, true), Times.Once); // 應該關閉窗口,并返回true }我們只需要驗證CloseItem被正確調用即可, 至于窗口是否能關閉那是Stylet需要確保的事了:)
使用Times.Never指定模擬的方法不應該被調用.(登錄驗證成功, 不顯示消息框)
驗證CloseItem(LoginViewModel, true)被調用了一次.
首先取得ViewModel的Parent, 這是一個實現了IChildDelegate的對象. 如未取到, 直接拋出異常.
否則調用IChildDelegate.CloseItem方法, 將自身和窗口返回值做為參數傳遞進去.
使用Moq來模擬一個IChildDelegate對象.
Setup一個CloseItem(LoginViewModel, true)方法.
將測試對象LoginViewModel的Parent設置為該模擬對象.
測試功能點: 點擊登錄窗口右上角的"X"按鈕,整個應用程序退出.
首先我們回憶一下該功能的代碼是怎么寫的:
protected override void OnViewLoaded() {var loginViewModel = _container.Get<LoginViewModel>();var result = _windowManager.ShowDialog(loginViewModel);if (result != true){RequestClose();} }接下來還有一個問題, 不知道你有沒有注意到, 就是OnViewLoaded是一個protected方法, 我們不能在測試代碼中直接調用ShellViewModel.OnViewLoaded, 那么該怎么辦呢? 我們的Act該怎么寫呢?
這里介紹一個常用的技巧, 我們創建一個類繼承ShellViewModel的類, 定義一個public方法, 并在該方法中調用ShellViewModel.OnViewLoaded. 因為該類是ShellViewModel的子類, 所以ShellViewModel的protected方法也可在子類中調用.代碼如下:
/// <summary> /// 為了測試ShellViewModel.OnViewLoaded方法而創建的類 /// </summary> public class ShellViewModelForTest : ShellViewModel {public ShellViewModelForTest(IContainer container, IWindowManager windowManager) : base(container, windowManager){}public void LoadView(){base.OnViewLoaded();} }至于其它的測試與Login中基本類似, 詳細的請看代碼.
該功能是在ShellViewModel的OnViewLoaded方法中實現的,所以這是Shell中的功能, 所以我們需要創建一個新的測試類ShellViewModelTest, 來測試該功能.
OnViewLoaded方法中同樣也使用了IWindowManager, 和RequestClose方法, 所以那些Moq的東西也少不了.
至此, 我們的測試代碼就寫完了. 可以看出使用MVVM模式, 對于界面邏輯的測試是很簡單的. 這也是MVVM備受推崇的原因.
本篇到此為止, 希望朋友們能多多留言. 源碼托管在GITHUB上.
Happy Coding~
總結
以上是生活随笔為你收集整理的【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 动手造轮子:实现一个简单的依赖注入(零)
- 下一篇: gRPC 流式调用