【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)相关推荐
- 项目挂到iis 点击导入 未将对象引用设置到对象的实例_【WPF on .NET Core 3.0】 Stylet演示项目 简易图书管理系统(2)...
上一章<[WPF on .NET Core 3.0] Stylet演示项目 - 简易图书管理系统(1)>中我们完成了一个简单的登录功能, 这一章主要演示如何对Stylet工程中的ViewM ...
- ASP.NET Core 2.0 Web API项目升级到ASP.NET Core 3.0概要笔记
本文结构 先决条件 升级目标框架(Target Framework)的版本 过时的IHostingEnvironment与IApplicationLifetime对象 Endpoint Routing ...
- .NET Core 2.0 特性介绍和使用指南
前言 这一篇会比较长,介绍了.NET Core 2.0新特性.工具支持及系统生态,现状及未来计划,可以作为一门技术的概述来读,也可以作为学习路径.提纲来用. 对于.NET Core 2.0的发布介绍, ...
- .NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新
我们都知道在6月12日的时候微软发布了.NET Core 3.0的第6个预览版.针对.NET Core 3.0的发布我们国内的微软MVP-汪宇杰还发布的官翻版的博文进行了详细的介绍.具体的可以关注&q ...
- 从头编写 asp.net core 2.0 web api 基础框架 (4) EF配置
第一部分: https://www.cnblogs.com/frank0812/p/11165940.html 第二部分:https://www.cnblogs.com/frank0812/p/111 ...
- .NET Core 3.0预览版7中的ASP.NET Core和Blazor更新
.NET Core 3.0 Preview 7现已推出,它包含一系列ASP.NET Core和Blazor的新更新. 以下是此预览中的新功能列表: 最新的Visual Studio预览包括.NET C ...
- .NET Core 3.0中的WinForms创建集中式拉取请求中心
Windows 窗体(或简称 WinForms),多年来被用于开发具有丰富和交互式界面的基于 Windows 的强大应用程序. 各类企业对这些桌面应用程序的投入量非常巨大,每月有大约 240 万开发人 ...
- 用WinForm/WPF代码来为.NET Core 3.0功能投票
我们在5月报道过微软希望在.NET Core 3.0上运行WinForms和WPF.为了实现这个目标,他们正在构建一个新工具,该工具将允许你投票以决定他们需要把哪些API移植到.NET Core.但是 ...
- .NET Core 3.0特性初探:C# 8、WPF、Windows Forms、EF Core
.NET Core的下一个主要版本最近进入了预览阶段,.NET Core 3.0将支持使用Windows Presentation Foundation (WPF).Windows Forms(Win ...
最新文章
- Tomcat关闭后,重新启动,session中保存的对象为什么还存在解决方法
- VSTO Office二次开发对PPT自定义任务窗格测试
- 【数论】排列组合学习笔记
- [转]SQL Server 2005 分区表实践——建立分区表(partition table)
- IBM向客户发放了一批“染毒”的U盘,现紧急建议物理销毁
- scikit-learn学习笔记(四)Ridge Regression ( 岭回归 )
- ASP.NET AJAX入门系列(10):Timer控件简单使用
- windows03系统安装08sql数据库
- titanium.cookbook-02-01-使用HTTPClient对象从远程XML读取数据
- ERROR: invalid byte sequence for encoding UTF8: 0xe5 0xb7 CONTEXT: COPY news_article, line 32973
- popwindow下拉筛选 二级联动_工作录入数据需要三级联动下拉菜单,Excel轻松制作!-Excel教程...
- STM32 CubeMX 串口通信
- 基于片内Flash的提示音播放程序
- 使用APICloud AVM多端框架开发课程表功能
- 【线程状态、等待与唤醒、Lambda表达式、Stream流】
- 领域自适应论文(六十八):Implicit Class-Conditioned Domain Alignment for Unsupervised Domain Adaptation论文原理
- 1960-征战的Loy
- 前TT(前T/T)与后TT(后T/T),以及信用证(LC,L/C)付款方式比较
- 马尔科夫不等式和坎泰利不等式的证明
- 大数据分析--用户画像