【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(2)

上一章《

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

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

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

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

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

那么我们就尝试编写代码来进行测试吧.

这里我们只测试ViewModel中的逻辑是否正确,对于UI测试则是另一个话题了,以后有机会再写.

创建测试工程

VS2019支持三种测试框架: MSTest, Nunit和xUnit, 功能上差不多, 你可以选择一个你喜欢的. 这里我们使用xUnit.

新建一个名为StyletBookStore.Test的xUnit Test Project(.NET Core)工程:

640?wx_fmt=png

然后对测试工程进行以下操作:

  • 添加对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<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.

  • 调用了LoginViewModelLogin方法.

  • 使用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方法, 将自身和窗口返回值做为参数传递进去.

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

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

  3. 将测试对象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中基本类似, 详细的请看代码.

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

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

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

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

Happy Coding~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313214.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MATLAB读取文件夹及其所有子文件夹内的图像

1。 指定路径下 单个文件夹data中所有图像 file_path .\data\;% 图像文件夹路径img_path_list dir(strcat(file_path,*.jpg));%获取该文件夹中所有jpg格式的图像img_num length(img_path_list);%获取图像总数量if img_num > 0 %有满足条件的图像for j 1:img_num %逐一读…

gRPC 流式调用

gRPC 使用 Protocol buffers 作为接口定义语言&#xff08;IDL&#xff09;来描述服务接口和输入输出消息的结构&#xff0c;目前支持 4 种定义服务方法类型&#xff1a;类型说明简单 RPC客户端传入一个请求对象&#xff0c;服务端返回一个结果对象客户端流式 RPC客户端传入多个…

模型压缩案例-SSDYou only look once

http://write.blog.csdn.NET/postedit 在上一篇文章中&#xff0c;介绍了以regionproposal来检测的框架&#xff0c;这一系列速度和精度不断提高&#xff0c;但是还是无法达到实时。存在的主要问题为&#xff1a;速度不够快&#xff0c;主要原因是proposal比较多&#xff0c;特…

.NET如何将字符串分隔为字符

前言如果这是一道面试题&#xff0c;答案也许非常简单&#xff1a;.ToCharArray()&#xff0c;这基本正确……我们以“AB吉??????”作为输入参数&#xff0c;首先如果按照“正常”处理的思路&#xff0c;用 .ToCharArray()&#xff0c;然后转换为 JSON&#xff08;以便方…

Rebuttal

http://blog.csdn.net/lqhbupt/article/details/25207463 1. Rebuttal是给编辑看的 2. 每个审稿人给出一个分数&#xff0c;加得总分 3. 定位。一般而言&#xff0c;对于area chair&#xff0c;那个给分比较低的会自然吸引他的眼球&#xff0c;相对占得的权重也就大&#xf…

Orleans 知多少 | 3. Hello Orleans

1. 引言是的&#xff0c;Orleans v3.0.0 已经发布了&#xff0c;并已经完全支持 .NET Core 3.0。所以&#xff0c;Orleans 系列是时候继续了&#xff0c;抱歉&#xff0c;让大家久等了。万丈高楼平地起&#xff0c;这一节我们就先来了解下Orleans的基本使用。2. 模板项目讲解在…

.NET Core 3.0之深入源码理解ObjectPool(二)

写在前面前文主要介绍了ObjectPool的一些理论基础&#xff0c;本文主要从源码角度理解Microsoft.Extensions.ObjectPool是如何实现的。下图为其三大核心组件图&#xff1a;核心组件ObjectPoolObjectPool是一个泛型抽象类&#xff0c;里面只有两个抽象方法&#xff0c;Get和Retu…

VC维学习

http://www.flickering.cn/machine_learning/2015/04/vc维的来龙去脉/ 说说历史Hoeffding不等式Connection to Learning学习可行的两个核心条件Effective Number of HypothesesGrowth FunctionBreak Point与ShatterVC BoundVC dimension深度学习与VC维小结参考文献 VC维在机器学…

.NET Core 3.0 一个 jwt 的轻量角色/用户、单个API控制的授权认证库

作者&#xff1a;痴者工良&#xff08;朋友合作原创&#xff09;来源&#xff1a;https://www.cnblogs.com/whuanle/p/11743406.html目录说明一、定义角色、API、用户二、添加自定义事件三、注入授权服务和中间件三、如何设置API的授权四、添加登录颁发 Token五、部分说明六、验…

.NET Core 3.0 构建和部署

Default Executables 默认可执行文件 在 dotnet build 或 dotnet publish 期间&#xff0c;将创建一个与你使用的 SDK 的环境和平台相匹配的可执行文件。 和其他本机可执行文件一样&#xff0c;可以使用这些可执行文件执行相同操作&#xff0c;例如&#xff1a; 可以双击可执行…

实现ZF-Net

根据的代码是https://github.com/hvy/chainer-visualization 注意&#xff1a;遇到问题一定要仔细看问题的描述&#xff0c;从最基本的描述入手&#xff0c;而不要按照网上的方法断章取义式的解决问题。 遇到的问题是&#xff1a; 1. 无法载入cupy库 解决办法&#xff1a; …

为什么我会了SOA,你们还要逼我学微服务?

菜菜哥&#xff0c;我最近需要做一个项目&#xff0c;老大让我用微服务的方式来做那挺好呀&#xff0c;微服务现在的确很流行我以前在别的公司都是以SOA的方式&#xff0c;SOA也是面向服务的方式呀的确&#xff0c;微服务和SOA有相同之处面向服务的架构&#xff08;SOA&#xf…

xorg.conf变更导致开机无法显示

不小心修改了ubuntu的/etc/X11中的xorg.conf文件导致开机无法显示&#xff0c; 于是尝试采用vim从开机界面修改回来&#xff1a; 安装vim: apt-get 编辑方法&#xff1a;先cd到路径下&#xff0c;ls -a列出目录下所有文件&#xff0c; 然后vi xorg.conf开始修改文件&#…

面对万物互联的智能世界,你是否也想分一杯羹

第六届世界互联网大会于10月20日至22日在浙江乌镇顺利举行。作为世界互联网大会“13”架构的重要组成部分&#xff0c;“互联网之光”博览会以“智能互联网、开放合作——携手共建网络空间命运共同体”为主题&#xff0c;集中展示了全球范围内的互联网新技术、新成果、新产品、…

All CUDA devices are used for display and cannot be used while debugging.

Fatal: All CUDA devices are used for display and cannot be used while debugging. (error codCUDBG_ERROR_ALL_DEVICES_WATCHDOGGED)(0x18) 问题解释&#xff1a;http://stackoverflow.com/questions/4025620/how-do-i-debug-a-cuda-library-with-only-1-graphics-card-ru…

你必须知道的容器监控 (2) cAdvisor

# 实验环境&#xff1a;阿里云ECS主机&#xff08;两台&#xff09;&#xff0c;CentOS 7.401—cAdvisor简介为了解决容器的监控问题&#xff0c;Google开发了一款容器监控工具cAdvisor&#xff08;Container Advisor&#xff09;&#xff0c;它为容器用户提供了对其运行容器的…

代码阅读

http://alanse7en.github.io/caffedai-ma-jie-xi-4/ 三. 从一个比较宏观的层面上去了解caffe怎么去完成一些初始化的工作和使用Solver的接口函数&#xff0c;本文将主要分为四部分的内容&#xff1a; Google Flags的使用Register Brew Function的宏的定义和使用train()函数的…

动手造轮子:实现一个简单的依赖注入(一)

动手造轮子&#xff1a;实现一个简单的依赖注入(一)Intro在上一篇文章中主要介绍了一下要做的依赖注入的整体设计和大概编程体验&#xff0c;这篇文章要开始写代码了&#xff0c;开始实现自己的依赖注入框架。类图首先来温习一下上次提到的类图服务生命周期服务生命周期定义&am…

Qt 调试Caffe

http://blog.csdn.net/xg123321123/article/details/52817658 1.下载并安装Qt Creator 下载页面&#xff0c;推荐使用4.x版本&#xff0c;比如&#xff1a; Qt Creator 4.1.0 for Linux 64-bit下载的是run包&#xff0c;安装方法&#xff1a; cd到下载目录 chmod x xxx.run …