上一章中我们完成了一个简单的登录功能, 这一章主要演示如何对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<LoginViewModel>().ToSelf();
    _container = builder.BuildContainer();
    }
    • Stylet的IoC容器需要使用StyletIoCBuilder提供的API来创建, 所以首先我们创建了StyletIoCBuilder的实例.

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

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

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

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

    先增加一个测试方法, 用来测试密码未输入时, 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的值

    测试"用户名未输入"和"用户名和密码都输入"的代码类似, 这里就不再详细说明了, 可直接看代码.

  3. 测试功能点: 用户名或密码不正确时, 显示"用户名或密码不正确"的消息框.

    因为登录逻辑中使用了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.
    • 调用了LoginViewModelLogin方法.
    • 使用Moq对象的Verify方法来验证模拟方法被调用了. Times.Once代表只调用了一次, 如果未调用或调用次数不是一次, Veryify方法会抛出异常.

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

  4. 测试功能点: 用户名输入"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方法, 将自身和窗口返回值做为参数传递进去.

    所以解决方案就出来了:

    1. 使用Moq来模拟一个IChildDelegate对象.
    2. Setup一个CloseItem(LoginViewModel, true)方法.
    3. 将测试对象LoginViewModel的Parent设置为该模拟对象.

    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()
    {
    // 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需要确保的事了:)

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

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

    protected override void OnViewLoaded()
    {
    var loginViewModel = _container.Get<LoginViewModel>();
    var result = _windowManager.ShowDialog(loginViewModel);
    if (result != true)
    {
    RequestClose();
    }
    }
    • 该功能是在ShellViewModelOnViewLoaded方法中实现的,所以这是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) - 单元测试的更多相关文章

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

    .NET Core 3.0已经发布了,除了一大堆令人激动的功能以外,也增加了对WPF的正式支持, 那么WPF在.NET Core 3.0下的开发体验如何呢? 本文利用了Stylet框架开发.NET C ...

  2. 【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(3) - 使用Conductor切换页面

    前两章中, 我们已经实现了这个图书管理系统的登录窗口, 并实施了完善的单元测试. 该是时候回过头来关注我们的主窗口了. 一个功能丰富的系统一般会有多个页面, 我们图书管理系统虽然是"简易&q ...

  3. 【WPF on .NET Core 3.0】 Stylet演示项目 - 简易图书管理系统(4) - 图书列表界面

    在前三章中我们完成了登录窗口, 并掌握了使用Conductor来切换窗口, 但这些其实都是在为我们的系统打基础. 而本章中我们就要开始开发系统的核心功能, 即图书管理功能了. 通过本章, 我们会接触到 ...

  4. Windows Forms和WPF在Net Core 3.0框架下并不会支持跨平台

    Windows Forms和WPF在Net Core 3.0框架下并不会支持跨平台 微软将WinForms和WPF带到.NET Core 3.0这一事实,相信大家都有所了解,这是否意味着它在Linux ...

  5. .Net Core .Net Core V1.0 创建MVC项目

    .Net Core V1.0 创建MVC项目 创建MVC项目有两种方式: 一.创建Web项目:(有太多没用的东西要去删太麻烦) 2.项目目录结构: 此种方法要注意的是,会创建好多个json文件,下面就 ...

  6. 用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 第三 ...

  7. .Net Core 3.0开源可视化设计CMS内容管理系统建站系统

    简介 ZKEACMS,又名纸壳CMS,是可视化编辑设计的内容管理系统.基于.Net Core开发可跨平台运行,并拥有卓越的性能. 纸壳CMS基于插件式设计,功能丰富,易于扩展,可快速创建网站. 布局设 ...

  8. .Net大局观(2).NET Core 2.0 特性介绍和使用指南

    .NET Core 2.0发布日期:2017年8月14日 前言 这一篇会比较长,系统地介绍了.NET Core 2.0及生态,现状及未来计划,可以作为一门技术的概述来读,也可以作为学习路径.提纲来用. ...

  9. .Net Core 2.0 生态(2).NET Core 2.0 特性介绍和使用指南

    .NET Core 2.0发布日期:2017年8月14日 前言 这一篇会比较长,介绍了.NET Core 2.0新特性.工具支持及系统生态,现状及未来计划,可以作为一门技术的概述来读,也可以作为学习路 ...

随机推荐

  1. 利用openssl自建CA体系

    使用 OpenSSL 创建私有 CA:1 根证书 使用 OpenSSL 创建私有 CA:2 中间证书 使用 OpenSSL 创建私有 CA:3 用户证书 今天跟着上面的三部曲,做了一下openssl的 ...

  2. 理解 Flutter 中的 Key

    概览 在 Flutter 中,大概大家都知道如何更新界面视图: 通过修改 Stata 去触发 Widget 重建,触发和更新的操作是 Flutter 框架做的. 但是有时即使修改了 State,Flu ...

  3. 修改so库中的依赖名

    修改so库中的依赖名 在ArchLinuxArm上有一些针对aarch64, arm, armeabi-v7a等Android常用架构的so库可以下载,有时候可以省去很多编译时间,且都是编译optim ...

  4. windows 下使用批处理执行 postgresql 命令行操作

    1.准备好命令文件 loraserver.sql create role loraserver_as with login password 'dbpassword'; create role lor ...

  5. mongodb基本安装

    这次搞搞NOSQL, 但最简单的MONGODB安装,还是要作点配置的. 一,安装网址: https://www.mongodb.com/download-center/community?jmp=na ...

  6. quarter软件的破解

    链接;http://www.openedv.com/forum.php?mod=viewthread&tid=275857&extra=page%3D1 这个是正点原子提供的破解方法, ...

  7. Go 数组(array) & 切片(slice)

    数组 数组是一组固定长度的序列 数组类型 数组的类型不仅和储存元素的类型有关,还和数组长度有关,不同长度的数组是不同的类型 不同类型的数组不能共用一个函数 func main() { var a [1 ...

  8. 记录自己的一次pjax性能优化

    什么是pjax? pjax = ajax + pushState 通过ajax让页面进行局部刷新,然后通过pushstate让url发生改变,再让pushState,让页面产生一个回退的记录,从而让页 ...

  9. 【第二章】Zabbix3.4监控SQLServer数据库和H3C交换机思科Cisco防火墙交换机教程笔记

    监控SQLServer数据库 SSMS执行相关SQL SQL模板命名规则 Zabbix客户端导入模板 添加SQLServer监控图形 SQLServer服务器关联模板 监控思科Cisco防火墙交换机 ...

  10. go语言变量作用域

    Go 语言变量作用域 作用域为已声明标识符所表示的常量.类型.变量.函数或包在源代码中的作用范围. Go 语言中变量可以在三个地方声明: 函数内定义的变量称为局部变量 函数外定义的变量称为全局变量 函 ...