简体   繁体   中英

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. 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. 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.

The problem comes when I want to unit test this 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?

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. I should have ItemListViewModel depend on mock IItemViewModels during testing.

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. 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. 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. 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.

Additionally, I've searched online and looked through the book Clean Code, but this really hasn't been covered. 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?

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.

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

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

var viewModel = factory.itemViewModel(addedItem);

Where factory provides a capability to create ItemViewModel, and the design allows us to provide substitutes.

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.

When is this important? One thing to notice is that you are asking about ItemViewModel, but you aren't asking about 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. 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. 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.

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".

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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