上一章中我们完成了一个简单的登录功能, 这一章主要演示如何对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. Effect:Mobile ocd

    Satisfy the following two Keep your phone at all times Check your phone even if there's no news Alwa ...

  2. android 引入一个布局库后该有的操作

    背景 引入一个布局库:com.zhy:percent-support-extends 然后sync now 成功了,也就是同步成功了. 然而开始使用的时候报告了: The following clas ...

  3. 速查 NSArray NSSet NSHashTable 快速遍历之速度比较

    因为NSArray中的指针并不是简单的连续存放的,所以简单的测试了Cocoa的三种集合的快速遍历(NSFastEnumeration)性能,给出简单的参考. 添加元素: [collection add ...

  4. Android Studio的安装及第一次启动时的配置

    Android Studio的安装及第一次启动时的配置 一.下载Android Studio 百度搜索“Android Studio" 点击中文社区进入,选择最新版本下载. 下载后双击安装包 ...

  5. Mac环境安装非APP STORE中下载的软件,运行报错:“XXX” is damaged and can’t be opened. You should move it to the Trash. 解决办法

    出现这个错误的大多数原因都是因为系统设置的问题,因为系统不信任你从其他地方下载的软件安装包,所以运行时就给你阻止了.具体的设置步骤如下: 1. 打开系统偏好设置 (System Preferences ...

  6. Linux系统学习 十五、VSFTP服务—匿名用户访问(不推荐使用,不安全)

    匿名用户访问 基本配置: anonymous_enable          #允许匿名用户访问 anon_upload_enable       #允许匿名用户上传 anon_mkdir_write ...

  7. faster-rcnn训练自己数据+测试

    准备使用faster-rcnn进行检测实验.同时笔者也做了mask-rcnn,yolo-v3,ssd的实验,并进行对比. window下使用faster-rcnn  https://blog.csdn ...

  8. BZOJ3894/LG4313 文理分科 新建点最小割

    问题描述 BZOJ3894 LG4313 题解 显然一个人只能选文/理 -> 一个人只能属于文(S).理(T)集合中的一个 可以把选择文得到 \(art\) 的收益看做选择文失去 \(scien ...

  9. flutter---安装教程

    下载java jdk  https://www.oracle.com/technetwork/java/javase/downloads/jdk13-downloads-5672538.html 下载 ...

  10. C# 对 Excel 的相关操作

    C# 对Excel的操作 学习自: 教练辅导 C# 对Excel的读取操作 我们需要额外添加引用: References 搜索Excel 这样我们的基础就添加完成了. 并且在using 中添加: us ...