Are you being militant? That is not really an easy question to answer. We do test the majority of our property changed events, of which there are a lot, and I'm not really sure how much value there is in those tests. By that I mean if we removed them and stopped writing them in the future would we start seeing more bugs, or even any that wouldn't be pretty obvious as soon as you used the client? To be honest the answer is probably no. Conversely they are easy tests to write and certainly don't hurt.
Anyway, yes there is a very nice way to do this (had to make a few minor tweeks, so can't guarantee the code will compile, but should make the concepts clear):
public static class PropertyChangedTestHelperFactory
{
/// <summary>
/// Factory method for creating <see cref="PropertyChangedTestHelper{TTarget}"/> instances.
/// </summary>
/// <param name="target">
/// The target.
/// </param>
/// <typeparam name="TTarget">
/// The target type.
/// </typeparam>
/// <returns>
/// The <see cref="PropertyChangedTestHelper{TTarget}"/>
/// </returns>
public static PropertyChangedTestHelper<TTarget> CreatePropertyChangedHelper<TTarget>(
this TTarget target)
where TTarget : INotifyPropertyChanged
{
return new PropertyChangedTestHelper<TTarget>(target);
}
}
public sealed class PropertyChangedTestHelper<TTarget> : IDisposable
where TTarget : INotifyPropertyChanged
{
/// <summary>
/// This list contains the expected property names that should occur in property change notifications
/// </summary>
private readonly Queue<string> propertyNames = new Queue<string>();
/// <summary>
/// The target of the helper
/// </summary>
private readonly TTarget target;
/// <summary>
/// Initialises a new instance of the <see cref="StrictPropertyChangedTestHelper{TTarget}"/> class.
/// </summary>
/// <param name="target">The target.</param>
public PropertyChangedTestHelper(TTarget target)
{
this.target = target;
this.target.PropertyChanged += this.HandleTargetPropertyChanged;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.target.PropertyChanged -= this.HandleTargetPropertyChanged;
if (this.propertyNames.Count != 0)
{
Assert.Fail("Property change notification {0} was not raised", this.propertyNames.Peek());
}
}
/// <summary>
/// Sets an expectation that a refresh change notification will be raised.
/// </summary>
public void ExpectRefresh()
{
this.propertyNames.Enqueue(string.Empty);
}
/// <summary>
/// Sets an expectation that a property change notification will be raised.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="propertyExpression">The property expression.</param>
public void Expect<TProperty>(Expression<Func<TTarget, TProperty>> propertyExpression)
{
this.propertyNames.Enqueue(((MemberExpression)propertyExpression.Body).Member.Name);
}
/// <summary>
/// Handles the target property changed event.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="PropertyChangedEventArgs"/> instance containing the event data.</param>
private void HandleTargetPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (this.propertyNames.Count == 0)
{
Assert.Fail("Unexpected property change notification {0}", e.PropertyName);
}
var expected = this.propertyNames.Dequeue();
var propertyName = (e.PropertyName ?? string.Empty).Trim();
if (propertyName != expected)
{
Assert.Fail("Out of order property change notification, expected '{0}', actual '{1}'", expected, propertyName);
}
}
}
Usage:
[TestMethod]
public void StatesIsSelected_RaisesIsValidChangeNotification()
{
// Arrange
var target = new SomeViewModel();
using (var helper = target.CreatePropertyChangedHelper())
{
helper.Expect(item => item.StatesIsSelected);
// Act
target.StatesIsSelected = true;
// Assert
}
}
When the helper is disposed the expectations are interrogated and the test will fail if they are not all met in the order they were defined.
We also have a Weak version that only requires that the expectations are met, not that they are met exactly (i.e. other property change events could be raised) and that is not order dependent.
FYI - if I were you I'd think about ditching MVVMLight and moving to Caliburn.Micro, its in a different league.