michael
michael

Reputation: 15302

What should you do about nested ViewModels when you are unit testing?

I was working on creating some unit tests for my ViewModels in my project. I didn't really have a problem as most of them were very simple, but ran into an issue when I had a new (unfinished) ViewModel inside of my other ViewModel.

public class OrderViewModel : ViewModelBase
{
    public OrderViewModel(IDataService dataService, int orderId)
    {
        \\ ...

        Payments = new ObservableCollection<PaymentViewModel>();
    }

    public ObservableCollection<PaymentViewModel> Payments { get; private set; }

    public OrderStatus Status { ... } //INPC

    public void AddPayment()
    {
        var vm = new PaymentViewModel();

        payments.Add(vm);

        // TODO: Subscribe to PaymentViewModel.OnPropertyChanged so that
        // if the payment is valid, we update the Status to ready.
    }
}

I want to create a unit test so that if any of my PaymentViewModel's IsValid property changes and all of them are true that Status should be OrderStatus.Ready. I can implement the class, but what gets me worried is that my unit test will break if the problem is in PaymentViewModel.

I'm not sure if this is OK or not, but it just feels like I should not have to worry about whether or not PaymentViewModel operates properly in order for my unit test for OrderViewModel is correct.

public void GivenPaymentIsValidChangesAndAllPaymentsAreValid_ThenStatusIsReady()
{
    var vm = new OrderViewModel();

    vm.AddPayment();
    vm.AddPayment();

    foreach (var payment in vm.Payments)
    {
        Assert.AreNotEqual(vm.Status, OrderStatus.Ready);

        MakePaymentValid(payment);
    }

    // Now all payments should be valid, so the order status should be ready.
    Assert.AreEqual(vm.Status, OrderStatus.Ready);
}

The problem is, how do I write MakePaymentValid in such a way that I guarantee that the PaymentViewModel's behavior will not negatively impact my unit test? Because if it does, then my unit test will fail based on another piece of code not working, rather than my code. Or, should it fail if PaymentViewModel is wrong as well? I am just torn in that I don't think that my tests for OrderViewModel should fail if PaymentViewModel has a bug.

I realize that I could always create an interface like how I do with IDataService, but it seems to me that that is a bit of an overkill to have every single ViewModel have an interface and injected in some how?

Upvotes: 2

Views: 119

Answers (1)

myermian
myermian

Reputation: 32525

When it comes to unit testing you absolutely should separate out your tests from any external dependencies. Keep in mind that this does not mean you must pass in some interface; you will run into situations where you have a specific class being utilized, whether that class is in or out of your control.

Imagine that instead of your example, you were relying on DateTime.Now. Some would argue to abstract it away into some sort of interface IDateTimeService, which could work. Alternatively, you could take advantage of Microsoft Fakes: Shims & Stubs.

Microsoft Fakes will allow you to create Shim* instances. There is a lot to go over on this subject, but the image Microsoft provides illustrates that the usage of Fakes goes beyond classes out of your control (it includes components within your control as well).

Microsoft Fakes Shims/Stubs

Notice how the component you are testing (OrderViewModel) should be isolated from System.dll (i.e. DateTime.Now), other components (PaymentViewModel), and external items as well (if you relied on a Database or Web Service). The Shim is for faking classes, whereas the Stub is for faking (mocking) interfaces.


Once you add a Fakes assembly, simply use the ShimPaymentViewModel class to provide the behavior you expect that it should. If, for whatever reason, the real PaymentViewModel class misbehaves and your application crashes you can at least be assured that the problem was not due to the OrderViewModel. Of course, to avoid that you should include some unit tests for PaymentViewModel to ensure that it behaves properly regardless of what other classes are utilizing it or how they are utilizing it.

TL;DR;

Yes, completely isolate your component when it comes to testing by taking advantage of Microsoft Fakes. Oh, and Microsoft Fakes does play nicely with other frameworks, so don't feel like that by using it that you are foregoing other options; it works in conjunction with other frameworks.

Upvotes: 3

Related Questions