A Fluent Interface For Testing INotifyPropertyChanged by Mark Seemann
If you are doing Rich UI, INotifyPropertyChanged is a pretty important interface. This is as true for WPF as it was for Windows Forms. Consisting solely of an event, it's not any harder to unit test than other events.
You can certainly write each test manually like the following.
[TestMethod] public void ChangingMyPropertyWillRaiseNotifyEvent_Classic() { // Fixture setup bool eventWasRaised = false; var sut = new MyClass(); sut.PropertyChanged += (sender, e) => { if (e.PropertyName == "MyProperty") { eventWasRaised = true; } }; // Exercise system sut.MyProperty = "Some new value"; // Verify outcome Assert.IsTrue(eventWasRaised, "Event was raised"); // Teardown }
Even for a one-off test, this one has a few problems. From an xUnit Test Patterns point of view, there's the issue that the test contains conditional logic, but that aside, the main problem is that if you have a lot of properties, writing all these very similar tests become old hat very soon.
To make testing INotifyPropertyChanged events easier, I created a simple fluent interface that allows me to write the same test like this:
[TestMethod] public void ChangingMyPropertyWillRaiseNotifyEvent_Fluent() { // Fixture setup var sut = new MyClass(); // Exercise system and verify outcome sut.ShouldNotifyOn(s => s.MyProperty) .When(s => s.MyProperty = "Some new value"); // Teardown }
You simply state for which property you want to verify the event when a certain operation is invoked. This is certainly more concise and intention-revealing than the previous test.
If you have interdependent properties, you can specify than an event was raised when another property was modified.
[TestMethod] public void ChangingMyPropertyWillRaiseNotifyForDerived() { // Fixture setup var sut = new MyClass(); // Exercise system and verify outcome sut.ShouldNotifyOn(s => s.MyDerivedProperty) .When(s => s.MyProperty = "Some new value"); // Teardown }
The When method takes any Action<T>, so you can also invoke methods, use Closures and what not.
There's also a ShouldNotNotifyOn method to verify that an event was not raised when a particular operation was invoked.
This fluent interface is implemented with an extension method on INotifyPropertyChanged, combined with a custom class that performs the verification. Here are the extension methods:
public static class NotifyPropertyChanged { public static NotifyExpectation<T> ShouldNotifyOn<T, TProperty>(this T owner, Expression<Func<T, TProperty>> propertyPicker) where T : INotifyPropertyChanged { return NotifyPropertyChanged.CreateExpectation(owner, propertyPicker, true); } public static NotifyExpectation<T> ShouldNotNotifyOn<T, TProperty>(this T owner, Expression<Func<T, TProperty>> propertyPicker) where T : INotifyPropertyChanged { return NotifyPropertyChanged.CreateExpectation(owner, propertyPicker, false); } private static NotifyExpectation<T> CreateExpectation<T, TProperty>(T owner, Expression<Func<T, TProperty>> pickProperty, bool eventExpected) where T : INotifyPropertyChanged { string propertyName = ((MemberExpression)pickProperty.Body).Member.Name; return new NotifyExpectation<T>(owner, propertyName, eventExpected); } }
And here's the NotifyExpectation class returned by both extension methods:
public class NotifyExpectation<T> where T : INotifyPropertyChanged { private readonly T owner; private readonly string propertyName; private readonly bool eventExpected; public NotifyExpectation(T owner, string propertyName, bool eventExpected) { this.owner = owner; this.propertyName = propertyName; this.eventExpected = eventExpected; } public void When(Action<T> action) { bool eventWasRaised = false; this.owner.PropertyChanged += (sender, e) => { if (e.PropertyName == this.propertyName) { eventWasRaised = true; } }; action(this.owner); Assert.AreEqual<bool>(this.eventExpected, eventWasRaised, "PropertyChanged on {0}", this.propertyName); } }
You can replace the Assertion with one that matches your test framework of choice (this one was written for MSTest).
Comments
e.g.
sut.ShouldNotifyOn(s => s.MyProperty).AndOn(s => s.MyDependentProperty).AndNotOn(s => s.MyIndependentProperty)
.When(s => s.MyProperty = "Some new value");