[英]Unit testing classes that instantiate other classes
我正在尝试为在其中实例化其他类的类编写单元测试,但正在努力解决如何以可测试的方式实例化这些类。 我知道依赖注入,但这有点不同,因为实例化不会在构造函数中发生。
这个问题确实不是 MVVM 和 C# 特有的,但这就是我的示例将使用的。 请注意,我已经简化了这一点,它不会按原样编译 - 目标是显示一个模式。
class ItemListViewModel
{
ItemListViewModel(IService service)
{
this.service.ItemAdded += this.OnItemAdded;
}
List<IItemViewModel> Items { get; }
OnItemAdded(IItemModel addedItem)
{
var viewModel = new ItemViewModel(addedItem);
this.Items.Add(viewModel);
}
}
class ItemViewModel : IItemViewModel
{
ItemViewModel(IItem) {}
}
从上面可以看出,模型层有一个事件。 ViewModel 侦听该事件,并作为响应添加一个新的子 ViewModel。 这符合我所知道的标准面向对象编程实践以及 MVVM 模式,对我来说感觉是一个非常干净的实现。
当我想对这个 ViewModel 进行单元测试时,问题就出现了。 虽然我可以使用依赖注入轻松模拟服务,但我无法模拟通过事件添加的项目。 这引出了我的主要问题:是否可以根据 ItemViewModel 的真实版本而不是模拟来编写我的单元测试?
我的直觉:这不行,因为我现在测试的不仅仅是 ItemListViewModel,特别是如果 ItemListViewModel 在内部调用任何项目的任何方法。 在测试期间,我应该让 ItemListViewModel 依赖于模拟 IItemViewModels。
我已经考虑了一些如何做到这一点的策略:
此外,我在网上搜索并浏览了 Clean Code 一书,但这确实没有被涵盖。 一切都在谈论依赖注入,这并没有明确解决这个问题。
是否可以根据 ItemViewModel 的真实版本而不是模拟来编写我的单元测试?
是的!
只要测试变得缓慢或设置起来非常复杂,您就应该使用真正的实现。
请注意,测试应该是隔离的,但要与其他测试隔离,而不是与被测单元的其他依赖项隔离。
测试的主要问题是应用程序使用共享状态(数据库、文件系统)。 共享状态使我们的测试相互依赖(添加和删除项目的测试不能并行运行)。 通过引入模拟,我们消除了测试之间的共享状态。
有时应用程序被划分为独立的域模块,这些模块通过抽象接口相互“通信”。 为了保持模块独立,我们将模拟通信接口,因此被测模块将不依赖于另一个域模块的实现细节。
从字面上模拟所有依赖项将使维护/重构更改成为一场噩梦,因为每次您将一些逻辑提取到专用类中时,您都将被迫更改/重写您正在重构的单元的测试套件。
您的场景是一个很好的例子,通过不ItemViewModel
创建,您将能够引入一个工厂将其注入到测试下的类中并运行现有的测试套件以确保工厂不会引入任何回归。
虽然我可以使用依赖注入轻松模拟服务,但我无法模拟通过事件添加的项目。
Misko Hevery 写了关于这种模式的文章: How to Think about the New Operator
如果您将应用程序逻辑与图形构造(新运算符)混合使用,那么除了应用程序中的叶节点之外,单元测试变得不可能。
因此,如果我们要查看您的问题代码:
OnItemAdded(IItemModel addedItem)
{
var viewModel = new ItemViewModel(addedItem);
this.Items.Add(viewModel);
}
那么我们可以考虑的一个变化是用更间接的方法替换对ItemViewModel::new
直接调用
var viewModel = factory.itemViewModel(addedItem);
factory
提供了创建 ItemViewModel 的能力,而设计允许我们提供替代品。
ItemListViewModel(IService service, Factory factory)
{
this.service.ItemAdded += this.OnItemAdded;
this.factory = factory;
}
完成后,您可以(在适当的时候)使用 Factory 来提供一些更简单的项目视图模型实现。
这什么时候重要? 需要注意的一件事是您问的是 ItemViewModel,但您不是问的是List 。 这是为什么?
几个答案:列表是稳定的; 我们完全不担心 List 本身的行为会以某种方式改变,从而导致 ItemListViewModel 的行为发生可观察到的变化。 如果测试问题后报告中,没有将是我们介绍我们的代码错误有任何疑问。
此外, this.List
(大概)是孤立的。 我们不必担心我们的测试结果会不稳定,因为同时运行了一些其他代码。 换句话说, are test 不容易受到共享可变状态引起的问题的影响。
如果这些属性也适用于 ItemViewModel,那么在您的代码中添加一堆仪式以在这两个实现之间创建分离实际上不会使您的设计变得“更好”。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.