简体   繁体   English

实例化其他类的单元测试类

[英]Unit testing classes that instantiate other classes

I'm trying to write unit tests for a class that instantiates other classes within it, but am struggling with how to instantiate those classes in a testable way.我正在尝试为在其中实例化其他类的类编写单元测试,但正在努力解决如何以可测试的方式实例化这些类。 I'm aware of dependency injection, but this is somewhat different as the instantiation doesn't happen in the constructor.我知道依赖注入,但这有点不同,因为实例化不会在构造函数中发生。

This question really isn't specific to MVVM and C#, but that's what my example will use.这个问题确实不是 MVVM 和 C# 特有的,但这就是我的示例将使用的。 Note that I've simplified this and it will not compile as-is - the goal is to show a pattern.请注意,我已经简化了这一点,它不会按原样编译 - 目标是显示一个模式。

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) {}
}

As can be seen above, there is an event from the model layer.从上面可以看出,模型层有一个事件。 The ViewModel listens to that event and, in response, adds a new child ViewModel. ViewModel 侦听该事件,并作为响应添加一个新的子 ViewModel。 This fits with standard Object Oriented programming practices that I'm aware of, as well as the MVVM pattern, and feels like quite a clean implementation to me.这符合我所知道的标准面向对象编程实践以及 MVVM 模式,对我来说感觉是一个非常干净的实现。

The problem comes when I want to unit test this ViewModel.当我想对这个 ViewModel 进行单元测试时,问题就出现了。 While I can easily mock out the service using dependency injection, I'm unable to mock out items added through the event.虽然我可以使用依赖注入轻松模拟服务,但我无法模拟通过事件添加的项目。 This leads to my primary question: is it OK to write my unit tests depending on the real version of ItemViewModel, rather than a mock?这引出了我的主要问题:是否可以根据 ItemViewModel 的真实版本而不是模拟来编写我的单元测试?

My gut feel: that isn't OK because I'm now inherently testing more than ItemListViewModel, particularly if ItemListViewModel calls any methods on any of the items internally.我的直觉:这不行,因为我现在测试的不仅仅是 ItemListViewModel,特别是如果 ItemListViewModel 在内部调用任何项目的任何方法。 I should have ItemListViewModel depend on mock IItemViewModels during testing.在测试期间,我应该让 ItemListViewModel 依赖于模拟 IItemViewModels。

There's a few tactics I've considered for how to do this:我已经考虑了一些如何做到这一点的策略:

  1. Have ItemListViewModel's owning class listen to the event and add mocked out items.让 ItemListViewModel 的拥有类监听事件并添加模拟项目。 This just moves the problem around, though, as now the owning class can't be fully mocked out.不过,这只是解决了问题,因为现在无法完全模拟拥有类。
  2. Pass an ItemViewModel factory into ItemListViewModel and use that instead of new.将 ItemViewModel 工厂传递到 ItemListViewModel 并使用它而不是新的。 This would definitely work for mocking as it moves things to be based on dependency injection...but does that mean I need a factory for every class I ever want to mock in my app?这肯定适用于模拟,因为它使事情基于依赖注入......但这是否意味着我需要为我想要在我的应用程序中模拟的每个类都有一个工厂? This feels wrong, and would be a pain to maintain.这感觉不对,维护起来会很痛苦。
  3. Refactor my model and how it communicates with the ViewModel.重构我的模型以及它如何与 ViewModel 通信。 Perhaps the eventing pattern I'm using isn't good for testing;也许我使用的事件模式不适合测试; though I don't see how I would get around needing to ultimately construct an ItemViewModel somewhere in code that needs to be tested.虽然我不知道我将如何解决最终需要在需要测试的代码中的某处构建一个 ItemViewModel 的问题。

Additionally, I've searched online and looked through the book Clean Code, but this really hasn't been covered.此外,我在网上搜索并浏览了 Clean Code 一书,但这确实没有被涵盖。 Everything talks about dependency injection, which doesn't clearly solve this.一切都在谈论依赖注入,这并没有明确解决这个问题。

is it OK to write my unit tests depending on the real version of ItemViewModel, rather than a mock?是否可以根据 ItemViewModel 的真实版本而不是模拟来编写我的单元测试?

Yes!是的!
You should use real implementation as long as tests become slow or very very complicated to setup.只要测试变得缓慢或设置起来非常复杂,您就应该使用真正的实现。

Notice that tests should be isolated, but isolated from other tests not from other dependencies of unit under the test.请注意,测试应该是隔离的,但要与其他测试隔离,而不是与被测单元的其他依赖项隔离。

Main issue of the testing is that applications using shared state (database, filesystem).测试的主要问题是应用程序使用共享状态(数据库、文件系统)。 Shared state makes our tests dependent on each other (tests for adding and removing items can not be run in parallel).共享状态使我们的测试相互依赖(添加和删除项目的测试不能并行运行)。 By introducing mocking we eliminate shared state between tests.通过引入模拟,我们消除了测试之间的共享状态。

Sometimes application being divided into independent domain modules, which "communicate" with each other via abstracted interface.有时应用程序被划分为独立的域模块,这些模块通过抽象接口相互“通信”。 To keep modules independent we will mock communication interface, so module under the test will not depend on implementation details of another domain module.为了保持模块独立,我们将模拟通信接口,因此被测模块将不依赖于另一个域模块的实现细节。

Mocking literally all dependencies will make maintenance/refactoring changes a nightmare, because every time you going to extract some logic into dedicated class you will be forced to change/rewrite test suit of the unit you are refactoring.从字面上模拟所有依赖项将使维护/重构更改成为一场噩梦,因为每次您将一些逻辑提取到专用类中时,您都将被迫更改/重写您正在重构的单元的测试套件。

Your scenario is good example, by not mocking creating of ItemViewModel , you will be able to introduce a factory inject it into the class under the test and run already existing test suit to make sure that factory didn't introduce any regressions.您的场景是一个很好的例子,通过不ItemViewModel创建,您将能够引入一个工厂将其注入到测试下的类中并运行现有的测试套件以确保工厂不会引入任何回归。

While I can easily mock out the service using dependency injection, I'm unable to mock out items added through the event.虽然我可以使用依赖注入轻松模拟服务,但我无法模拟通过事件添加的项目。

Misko Hevery wrote about this pattern: How to Think About the New Operator Misko Hevery 写了关于这种模式的文章: How to Think about the New Operator

If you mix application logic with graph construction (the new operator) unit-testing becomes impossible for anything but the leaf nodes in your application.如果您将应用程序逻辑与图形构造(新运算符)混合使用,那么除了应用程序中的叶节点之外,单元测试变得不可能。

So if we were to look at your problem code:因此,如果我们要查看您的问题代码:

OnItemAdded(IItemModel addedItem)
{
  var viewModel = new ItemViewModel(addedItem);
  this.Items.Add(viewModel);
}

then one change we could consider is replacing this direct call to ItemViewModel::new with a more indirect approach那么我们可以考虑的一个变化是用更间接的方法替换对ItemViewModel::new直接调用

var viewModel = factory.itemViewModel(addedItem);

Where factory provides a capability to create ItemViewModel, and the design allows us to provide substitutes. factory提供了创建 ItemViewModel 的能力,而设计允许我们提供替代品。

ItemListViewModel(IService service, Factory factory)
{
  this.service.ItemAdded += this.OnItemAdded;
  this.factory = factory;
}

Having done that, you can (when appropriate) use a Factory that provides some simpler implementation of your item view model.完成后,您可以(在适当的时候)使用 Factory 来提供一些更简单的项目视图模型实现。

When is this important?这什么时候重要? One thing to notice is that you are asking about ItemViewModel, but you aren't asking about List .需要注意的一件事是您问的是 ItemViewModel,但您不是问的是List Why is that?这是为什么?

A couple of answers: List is stable;几个答案:列表是稳定的; we aren't at all worried that the behavior of List itself is going to change in a way that causes an observable change to the behavior of ItemListViewModel.我们完全不担心 List 本身的行为会以某种方式改变,从而导致 ItemListViewModel 的行为发生可观察到的变化。 If the test reports a problem later, there isn't going to be any doubt that we introduced a mistake in our code.如果测试问题后报告中,没有将是我们介绍我们的代码错误有任何疑问。

Also, this.List is (presumably) isolated.此外, this.List (大概)是孤立的。 We don't have to worry that our test results are going to be flaky because some other code is running at the same time.我们不必担心我们的测试结果会不稳定,因为同时运行了一些其他代码。 In other words, are test is not vulnerable to problems caused by shared mutable state.换句话说, are test 不容易受到共享可变状态引起的问题的影响。

If those properties also hold for ItemViewModel, then adding a bunch of ceremony to your code to create separation between these two implementations isn't actually going to make your design any "better".如果这些属性也适用于 ItemViewModel,那么在您的代码中添加一堆仪式以在这两个实现之间创建分离实际上不会使您的设计变得“更好”。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM