上一章《【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(1)》中我们完成了一个简单的登录功能, 这一章主要演示如何对Stylet工程中的ViewModel进行单元测试.

回忆一下我们的登录逻辑,主要有以下4点:

  1. 当"用户名"或"密码"为空时, 是不允许登录的("登录"按钮处于禁用状态).

  2. 用户名或密码不正确时, 显示"用户名或密码不正确"的消息框.

  3. 用户名输入"waku", 并且密码输入"123", 登录成功窗口关闭, 回到主窗口.

  4. 点击登录窗口右上角的"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的类, 在其中编写测试代码.

  1. 配置Stylet的IoC容器

    因为我们的LoinViewModel使用了依赖注入,所以在测试代码中最好也是使用IoC来创建测试对象.在LoginViewModelTest的构造方法中增加以下代码:

    public LoginViewModelTest(){// 向Stylet的IoC中注册服务var builder = new StyletIoCBuilder();    builder.Bind().ToSelf();    _container = builder.BuildContainer();}
  • Stylet的IoC容器需要使用StyletIoCBuilder提供的API来创建, 所以首先我们创建了StyletIoCBuilder的实例.

  • 使用Bind范型方法注册服务, 这里我们将LoginViewModel的自身注册进去.

    更多关于Stylet的IoC配置方法请浏览WIKI

  • 最后使用BuildContainer方法创建IoC容器, 由于我们需要在测试方法中使用该容器,所以需要定义一个成员变量来存储它:

    private readonly IContainer _container;

测试功能点: 当"用户名"或"密码"为空时, 是不允许登录的("登录"按钮处于禁用状态).

先增加一个测试方法, 用来测试密码未输入时, CanLogin应该返回false:

/// /// 密码未输入, 不允许点击登录/// [Fact]public void CanLoginTest_NoPassword(){// Arrangevar vm = _container.Get();    vm.UserName = "waku";    vm.Password = String.Empty;// Actbool canLogin = vm.CanLogin;// Assert    canLogin.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();    _mockWindowManager.Setup(_showMessageBoxExpr).Returns(MessageBoxResult.OK);    ...    builder.Bind().ToInstance(_mockWindowManager.Object);    // 注册IWindowManager    ...}

有了Mock对象, 我们就可以来编写验证登录逻辑的测试代码了:

/// /// 用户名错误/// [Fact]public void LoginTest_WrongUserName(){// Arrangevar vm = _container.Get();    vm.UserName = "wrong_username";    vm.Password = "123";// Act    vm.Login();// Assert    _mockWindowManager.Verify(_showMessageBoxExpr, Times.Once); // 应该显示消息框}

还需要测试用户名正确但是密码不正确的情形, 就不详细说明了.

  • 我们设置了一个错误的用户名wrong_username.

  • 调用了LoginViewModelLogin方法.

  • 使用Moq对象的Verify方法来验证模拟方法被调用了. Times.Once代表只调用了一次, 如果未调用或调用次数不是一次, Veryify方法会抛出异常.

  • 使用new Mock来创建一个Mock对象, T即是要Mock的实际类型. 后续我们需要使用Mock对象_mockWindowManager, 所以将其定义为一个成员变量:

    private readonly Mock _mockWindowManager;
  • 我们使用Moq的Setup方法来为指定的接口模拟一个方法, 该方法接收一个Expression类型的值. 为了简洁性, 我们将Expression定义为一个成员变量:

    private readonly Expression> _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是如何实现的:

/// /// Request that the conductor responsible for this screen close it/// /// DialogResult to return, if this is a dialogpublic 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 _mockWindowManager;    ...public LoginViewModelTest(){        ...// 使用Moq虚拟IChildDelegate        _mockChildDelegate = new Mock();        ...        builder.Bind().ToInstance(_mockChildDelegate.Object);    // 注册IChildDelegate        ...    }

测试方法:

/// /// 正确的用户名和密码/// [Fact]public void LoginTest(){// Arrangevar vm = _container.Get();var childDelegate = _container.Get();    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}

我们只需要验证CloseItem被正确调用即可, 至于窗口是否能关闭那是Stylet需要确保的事了:)

  • 使用Times.Never指定模拟的方法不应该被调用.(登录验证成功, 不显示消息框)

  • 验证CloseItem(LoginViewModel, true)被调用了一次.

  • 首先取得ViewModel的Parent, 这是一个实现了IChildDelegate的对象. 如未取到, 直接抛出异常.

  • 否则调用IChildDelegate.CloseItem方法, 将自身和窗口返回值做为参数传递进去.

  1. 使用Moq来模拟一个IChildDelegate对象.

  2. Setup一个CloseItem(LoginViewModel, true)方法.

  3. 将测试对象LoginViewModel的Parent设置为该模拟对象.

测试功能点: 点击登录窗口右上角的"X"按钮,整个应用程序退出.

首先我们回忆一下该功能的代码是怎么写的:

protected override void OnViewLoaded(){var loginViewModel = _container.Get();var result = _windowManager.ShowDialog(loginViewModel);if (result != true)    {        RequestClose();    }}

接下来还有一个问题, 不知道你有没有注意到, 就是OnViewLoaded是一个protected方法, 我们不能在测试代码中直接调用ShellViewModel.OnViewLoaded, 那么该怎么办呢? 我们的Act该怎么写呢?

这里介绍一个常用的技巧, 我们创建一个类继承ShellViewModel的类, 定义一个public方法, 并在该方法中调用ShellViewModel.OnViewLoaded. 因为该类是ShellViewModel的子类, 所以ShellViewModel的protected方法也可在子类中调用.代码如下:

/// /// 为了测试ShellViewModel.OnViewLoaded方法而创建的类/// public class ShellViewModelForTest : ShellViewModel{public ShellViewModelForTest(IContainer container, IWindowManager windowManager) : base(container, windowManager){    }

public void LoadView(){base.OnViewLoaded();    }}

至于其它的测试与Login中基本类似, 详细的请看代码.

  • 该功能是在ShellViewModelOnViewLoaded方法中实现的,所以这是Shell中的功能, 所以我们需要创建一个新的测试类ShellViewModelTest, 来测试该功能.

  • OnViewLoaded方法中同样也使用了IWindowManager, 和RequestClose方法, 所以那些Moq的东西也少不了.

至此, 我们的测试代码就写完了. 可以看出使用MVVM模式, 对于界面逻辑的测试是很简单的. 这也是MVVM备受推崇的原因.

本篇到此为止, 希望朋友们能多多留言. 源码托管在GITHUB上.

Happy Coding~

项目挂到iis 点击导入 未将对象引用设置到对象的实例_【WPF on .NET Core 3.0】 Stylet演示项目 简易图书管理系统(2)...相关推荐

  1. 项目挂到iis 点击导入 未将对象引用设置到对象的实例_用Notion管理读书项目

    Notion是什么 Notion是一款全能型笔记应用,你可以用它排版.管理笔记.管理工作项目.建立数据库.建立Wiki档案库,甚至免费构建出一个网站来.不太夸张的说,只有你想不到,没有它做不到的.如果 ...

  2. mysql未将对象引用设置到对象的实例_记一次未将对象引用设置到对象的实例问题的排查过程...

    最近在给一个老项目做数据对接接口. 背景一 该项目最后更新日期为2006年,使用ASP.NET WebForm..Net2.0.OJB.Castle Avtive Record等.由于是某集团的子系统 ...

  3. mysql安装 未将对象引用设置到对象的实例._未将对象引用设置到对象的实例--可能出现的问题总结...

    一.网络上的一般说法 1.ViewState 对象为Null. 2.DateSet 空. 3.sql语句或Datebase的原因导致DataReader空. 4.声明字符串变量时未赋空值就应用变量. ...

  4. mysql 未将对象引用设置到对象的实例_未将对象引用设置到对象的实例 总结

    三.解决方案: 1局部调试:设置断点.在浏览器打开要调试的页面.若断点调试无效,则: 2使用进程调试:工具-附加到进程-选中当前要调试的进程(通常是w3wp)-- 运行(当前主进程) 一.常见原因 1 ...

  5. python未将对象引用设置到对象的实例_在Python中使用pingarapi。服务器引发Webfault:对象引用未设置为对象的实例...

    大家晚上好. 我想说的是,我对wsdl.soap.suds这整件事都很陌生!尽可能多地提供信息.wsdl是这个http://api3.pingar.com/PingarAPIService.asmx? ...

  6. sqlserver2008未将对象引用设置到对象的实例_面试官:ThreadLocal 的内存泄漏是弱引用导致的,你确定?...

    面试官:ThreadLocal 了解吗? Python 小星:线程局部变量,多线程下能保证各个线程的变量相对独立于其他线程的变量. 面试官:那你说下它是如何保证线程隔离的? Python 小星:每个线 ...

  7. python未将对象引用设置到对象的实例_未将对象引用设置到对象的实例(SystemNullReferenceException)...

    {"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],&q ...

  8. C# Linq 未将对象引用设置到对象的实例

    在C# + MVC项目中用LINQ 操作数据库,提示 未将对象引用设置到对象的实例 调查发现是 查询的字段存在 NUll 值我们需要先设置该字段  != null 在进行 == . contains ...

  9. 新建WindowsPhone项目时提示未将对象引用设置到对象的实例

    问题: 安装好新系统之后(只有Windows8 专业版和企业版支持hyper-v),然后安装vs2012,再安装Wp8 Sdk,安装完毕后新建Windows Phone项目,会提示未将对象引用设置到对 ...

最新文章

  1. java中toarray()的 用法_java容器中toArray的用法
  2. 5h Oralcle进阶直播课,限时免费报名,手慢无!
  3. 前端开源项目周报0214
  4. ARM汇编中ldr伪指令和ldr指令(转载)
  5. java怎样调用图像做按钮_swing-Java:使用图像作为按钮
  6. 线程池是如何重复利用空闲线程的?
  7. 出租车计价器设计VHDL
  8. 2022-02-23 安卓开发七年面试题总结
  9. mvvm与virtual dom算法的实践——“hoz”
  10. eclipse访问限制
  11. react 日期怎么格式化_react日期格式化组件
  12. Petya and Countryside
  13. Linux编写简单的脚本
  14. 安装centos7.0时电脑进入黑屏的解决方法
  15. 面向深度学习系统的对抗样本攻击与防御
  16. JAVA 开发相关软件介绍以及安装使用教程(初学者一定要收藏)
  17. 电脑显示服务器意外终止啥意思,电脑提示“Dcom Server Process Launcher服务意外终止”怎么办...
  18. 需求为纲 产品为王 极米担当行业领头羊的秘密
  19. 网络编程基础socket 重要中:TCP/UDP/七层协议
  20. ffmpeg有gpl和lgpl版本,jetson硬件加速

热门文章

  1. 【死磕NIO】— NIO基础详解
  2. Liferay7.0 MVCPortlet部署
  3. WINDOWS下如何安装GCC
  4. graalvm 拯救生命,速速入手
  5. 方法论:带着问题找答案
  6. 简述计算机病毒的清除,人工清除计算机病毒的方法
  7. html5 游戏转换为小游戏,HTML5小游戏转换成mobile应用
  8. 康耐视visionpro工具-卡尺工具-Caliper-简介
  9. js 加快播放视频速度
  10. C++ set 交集 并集 差集