【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(2) - 单元测试
上一章中我们完成了一个简单的登录功能, 这一章主要演示如何对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容器
因为我们的
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()
{
// 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要求所有测试方法需要有
测试功能点: 用户名或密码不正确时, 显示"用户名或密码不正确"的消息框.
因为登录逻辑中使用了
IWindowManager来显示消息框, 这里我们需要利用Moq来模拟它.在LoginViewModelTest构造方法中增加以下代码:public LoginViewModelTest()
{
// 使用Moq虚拟IWindowManager
_mockWindowManager = new Mock<IWindowManager>();
_mockWindowManager.Setup(_showMessageBoxExpr).Returns(MessageBoxResult.OK); ...
builder.Bind<IWindowManager>().ToInstance(_mockWindowManager.Object); // 注册IWindowManager
...
}
使用
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属性可以取得模拟对象.
有了Mock对象, 我们就可以来编写验证登录逻辑的测试代码了:
/// <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)方法.- 将测试对象
LoginViewModel的Parent设置为该模拟对象.
Mock相关的代码如下, 与Mock
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)被调用了一次.
我们只需要验证
CloseItem被正确调用即可, 至于窗口是否能关闭那是Stylet需要确保的事了:)- 首先取得ViewModel的Parent, 这是一个实现了
测试功能点: 点击登录窗口右上角的"X"按钮,整个应用程序退出.
首先我们回忆一下该功能的代码是怎么写的:
protected override void OnViewLoaded()
{
var loginViewModel = _container.Get<LoginViewModel>();
var result = _windowManager.ShowDialog(loginViewModel);
if (result != true)
{
RequestClose();
}
}
- 该功能是在
ShellViewModel的OnViewLoaded方法中实现的,所以这是Shell中的功能, 所以我们需要创建一个新的测试类ShellViewModelTest, 来测试该功能. OnViewLoaded方法中同样也使用了IWindowManager, 和RequestClose方法, 所以那些Moq的东西也少不了.
接下来还有一个问题, 不知道你有没有注意到, 就是
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中基本类似, 详细的请看代码.
- 该功能是在
至此, 我们的测试代码就写完了. 可以看出使用MVVM模式, 对于界面逻辑的测试是很简单的. 这也是MVVM备受推崇的原因.
本篇到此为止, 希望朋友们能多多留言. 源码托管在GITHUB上.
Happy Coding~
【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(2) - 单元测试的更多相关文章
- 【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(1)
.NET Core 3.0已经发布了,除了一大堆令人激动的功能以外,也增加了对WPF的正式支持, 那么WPF在.NET Core 3.0下的开发体验如何呢? 本文利用了Stylet框架开发.NET C ...
- 【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(3) - 使用Conductor切换页面
前两章中, 我们已经实现了这个图书管理系统的登录窗口, 并实施了完善的单元测试. 该是时候回过头来关注我们的主窗口了. 一个功能丰富的系统一般会有多个页面, 我们图书管理系统虽然是"简易&q ...
- 【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(4) - 图书列表界面
在前三章中我们完成了登录窗口, 并掌握了使用Conductor来切换窗口, 但这些其实都是在为我们的系统打基础. 而本章中我们就要开始开发系统的核心功能, 即图书管理功能了. 通过本章, 我们会接触到 ...
- Windows Forms和WPF在Net Core 3.0框架下并不会支持跨平台
Windows Forms和WPF在Net Core 3.0框架下并不会支持跨平台 微软将WinForms和WPF带到.NET Core 3.0这一事实,相信大家都有所了解,这是否意味着它在Linux ...
- .Net Core .Net Core V1.0 创建MVC项目
.Net Core V1.0 创建MVC项目 创建MVC项目有两种方式: 一.创建Web项目:(有太多没用的东西要去删太麻烦) 2.项目目录结构: 此种方法要注意的是,会创建好多个json文件,下面就 ...
- 用VSCode开发一个asp.net core 2.0+angular 5项目(4): Angular5全局错误处理
第一部分: http://www.cnblogs.com/cgzl/p/8478993.html 第二部分: http://www.cnblogs.com/cgzl/p/8481825.html 第三 ...
- .Net Core 3.0开源可视化设计CMS内容管理系统建站系统
简介 ZKEACMS,又名纸壳CMS,是可视化编辑设计的内容管理系统.基于.Net Core开发可跨平台运行,并拥有卓越的性能. 纸壳CMS基于插件式设计,功能丰富,易于扩展,可快速创建网站. 布局设 ...
- .Net大局观(2).NET Core 2.0 特性介绍和使用指南
.NET Core 2.0发布日期:2017年8月14日 前言 这一篇会比较长,系统地介绍了.NET Core 2.0及生态,现状及未来计划,可以作为一门技术的概述来读,也可以作为学习路径.提纲来用. ...
- .Net Core 2.0 生态(2).NET Core 2.0 特性介绍和使用指南
.NET Core 2.0发布日期:2017年8月14日 前言 这一篇会比较长,介绍了.NET Core 2.0新特性.工具支持及系统生态,现状及未来计划,可以作为一门技术的概述来读,也可以作为学习路 ...
随机推荐
- Cookie插件的使用
jQuery-cookie插件的使用 什么是插件? 基于jQuery的语法,按照一定规范书写,具有特定功能的脚本文件,称为插件. 插件除了js文件之外,有的还包含css文件,图片和字体等资源文件. 在 ...
- 在Electron中最快速预加载脚本
背景 在Electron打开新窗口的时候,提前加载一段JavaScript脚本,以此内置一些属性或接口给被打开的页面.之所以要以注入方式,而不是页面自己引用,原因是不想麻烦页面自行引用,不想修改旧有的 ...
- Hyper-V “SP2019SER”无法更改状态。操作失败,错误代码为“32788”。
卸载Hyper-V,然后重装,再重启已有的Hyper-V服务器,报错如下: 尝试启动选定的虚拟机时出错.“SP2019SER”无法更改状态. 原因:卸载后导致虚拟网卡出现问题导致的. 解决办法: 右击 ...
- 推荐一个好用的行内可编辑的table组件 vxe-table
项目中有一个需要用户点击table单元格可编辑的需求,由于博主用的是elementUI,element组件内实现可编辑,用过的小伙伴都知道,非常麻烦,后来博主在浏览组件的时候发现了 一款非常好用的ta ...
- 腾讯云服务器ubuntu18.04部署禅道系统
踩了不少坑,记录一下. 基于ubuntu18.04 一开始按照网上的攻略下载安装包 ZenTaoPMS.9.8.3.zbox_64.tar.gz,通过FileZilla传到linux的/opt下面,解 ...
- ES6 ES7 ES8 相关用法
set Set作为ES6新的数据解构(类数组),它的成员都是唯一的,因为最直接的使用场景便是去重.并.差.交集的使用.它使用的算法叫做“Same-value-zero equality”,类似精确运算 ...
- Redis与Redis 伪集群环境的搭建
一 .准备工作 GCC编译环境 ruby运行环境 安装ruby脚本运行包 二.环境安装 1.GCC环境 首先,因为redis是由C语言编写的,所以需要安装GCC环境,可以用 gcc -v 命令来检查是 ...
- Java连载55-接口的作用、接口举例
一.接口的作用 1.可以使项目分层,所有层都面向接口开发,开发效率提高了. 2.接口使代码和代码之间的耦合度降低,就像内存条和主板的关系,变得“可插拔”,可以随意切换. 总结:接口和抽象类能够完成某 ...
- yii2 提示
控制器中: Yii::$app->getSession()->setFlash('success', "描述!"); Yii::$app->getSession( ...
- Springcloud 配置 | 史上最全,一文全懂
Springcloud 高并发 配置 (一文全懂) 疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列之15 [博客园总入口 ] 前言 疯狂创客圈(笔者尼恩创建的高并发研习社群)Spring ...