上一章中我们完成了一个简单的登录功能, 这一章主要演示如何对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. js 回调地狱的另类解决方案尝试

    例如 通过学生获取学生所在学校信息,需要先查询学生所在班级,再通过班级查询所在学校信息.js代码类似写法如下: function getStudentSchool(id) { ajax.get(&qu ...

  2. 利用openssl自建CA体系

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

  3. 使用Visual Studio Code进行远程开发

    微软的VS code能够适应不同开发环境,提供对多种语言的支持,使得使用VS code开发变得很流行了.因为各种原因(比如在本地设置开发环境困难,或者繁琐,或者开发环境没有图形界面),我们可能需要远程 ...

  4. Python语法速查: 13. 操作系统服务

    返回目录 本篇索引 (1)sys模块 (2)os模块 (3)与Windows相关模块 (4)subprocess模块 (5)signal模块 (1)sys模块 sys模块用于Python解释器及其环境 ...

  5. Python语法速查: 12. 文件与输入输出

    返回目录 (1)文件基本操作 ● 文件常用操作 内置函数或方法 描述 open(name [,mode [,buffering]]) 内置函数.用来打开文件,返回一个文件对象(file对象).详见下述 ...

  6. Spring学习的第三天

    问题分析:在刚开始进行银行转账案例时,会获取四个连接,分别是查询接入.转出账户.更新转入.转出账户.这样会使转账不同步,如果某一处出现异常,前面的代码执行了,而后面的更新却没执行,导致一个账户加钱而另 ...

  7. Spring Boot可执行Jar包运行原理

    目录 1. 打可执行Jar包 2. 可执行Jar包内部结构 3. JarLauncher 4. 简单总结 5. 远程调试 Spring Boot有一个很方便的功能就是可以将应用打成可执行的Jar.那么 ...

  8. linux中服务(service)管理

    一.介绍 服务(service) 本质就是进程,但是是运行在后台的,通常都会监听某个端口,等待其它程序的请求,比如(mysql , sshd 防火墙等),因此我们又称为守护进程,是Linux 中非常重 ...

  9. 使用Navicat Premium 比较PostgreSql数据库 dev环境与test环境差异

    Navicat Premium 功能很强大,支持不同数据库客户端的连接,并且使用工具可以生成两个库差异的sql脚本,方便dev与test环境表结构同步,具体操作方法如下 单击运行,实现两个库中模式表结 ...

  10. Bootstrap基本CSS样式

    一.简介.使用 1.简介 Bootstrap 来源于 Twitter,是一款基于 Html.Css.JavaScript 的前端UI框架.可以方便.快速的开发web界面. 教程:https://www ...