Reputation: 15302
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
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).
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