是否可以根据实际版本编写单元测试ItemViewModel,而不是模拟?
我正在尝试为一个实例化其中的其他类的类编写单元测试,但是正在努力以一种可测试的方式实例化那些类。我知道依赖注入,但这有所不同,因为实例化没有在构造函数中发生。
这个问题确实不是特定于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。
[我已经考虑过一些战术来做到这一点:
[此外,我已经在网上搜索并浏览了《清洁代码》一书,但这确实没有涉及到。所有人都在谈论依赖注入,但这并不能清楚地解决这个问题。
尽管我可以轻松地使用依赖项注入来模拟服务,但无法模拟通过事件添加的项目。
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
被隔离。我们不必担心我们的测试结果会很不稳定,因为其他一些代码正在同时运行。换句话说,测试不容易受到共享可变状态引起的问题的影响。
[如果这些属性也适用于ItemViewModel,则在代码中添加一堆仪式来创建这两个实现之间的分离实际上并不会使您的设计变得“更好”。
是否可以根据实际版本编写单元测试ItemViewModel,而不是模拟?
是!
只要测试变慢或设置非常复杂,就应该使用真实的实现。注意,测试应隔离,但应与其他测试隔离,而不能与受测单元的其他依赖项隔离。
测试的主要问题是应用程序使用共享状态(数据库,文件系统)。共享状态使我们的测试彼此依赖(添加和删除项目的测试不能并行运行)。通过引入模拟,我们消除了测试之间的共享状态。
有时,应用程序被划分为独立的域模块,它们通过抽象的接口彼此“通信”。为了使模块保持独立,我们将模拟通信接口,因此受测模块将不依赖于另一个域模块的实现细节。
从字面上嘲笑所有依赖项都会使维护/重构更改成为一场噩梦,因为每次您将某些逻辑提取到专用类中时,都会被迫更改/重写要重构的单元的测试套件。
是否可以根据实际版本编写单元测试ItemViewModel,而不是模拟?