loading...

Implementing a Property Change Tracker For Testing in C#

buddylreno profile image Buddy Reno ・4 min read

While testing a view model I was creating for a form in an app, I found myself needing the ability to test that a boolean value was being transformed a couple times during a function. I looked into Fluent Assertions (the testing library I was using) and around Google, but didn't find any out-of-the-box solutions.

Since this was a view model that implemented INotifyPropertyChanged, I knew I could tie into the event and make my own tracker! Here's how I created that.

Setup

Let's create a class to start building from.

// We'll need only 2 libraries
using System.Collections.Generic;
using System.ComponentModel;

namespace SharedUtilities.Testing
{
  public PropertyChangeTracker() 
  {
  }
}

The first thing we need to do is to consume a view model in the constructor that implements INotifyPropertyChanged. If you aren't sure what this is, check out the docs here.

namespace SharedUtilities.Testing
{
  private INotifyPropertyChanged _vmInstance;  

  public PropertyChangeTracker(INotifyPropertyChanged vmInstance) 
  {
    _vmInstance = vmInstance;
  }
}

PropertyChangeTracker, utilizes constructor injection to take advantage of inversion of control with an object that implements INotifyPropertyChanged. The argument is then stored in a private variable. Now a "tracker" listener can be assigned to the PropertyChanged event.

public PropertyChangeTracker(INotifyPropertyChanged vmInstance) 
{
  _vmInstance = vmInstance;
  _vmInstance.PropertyChanged += Tracker;
}

private void Tracker(object sender, PropertyChangedEventArgs e) 
{
}

It's important to declare the Tracker function with PropertyChangedEventArgs as opposed to EventArgs. This allows access to the PropertyName variable.

Creating The Tracker

Let's use the PropertyName variable from PropertyChangedEventArgs to gain access to the value of the changed property.

//inside the Tracker method

var prop = sender.GetType().GetProperty(e.PropertyName);
var value = prop?.GetValue(sender);

Awesome! Now we have access to the property name through e.PropertyName as well as the value using the GetValue method! The next step is to create a class that can store the results to give back to the test using this tracker.

public class PropertyChangeTracker
{
  // CODE
}

public class PropertyChangedInfo 
{
  public string PropertyName { get; set; }
  public object Value { get; set; }
}

Note that the Value property is specified as being an object. because we don't know anything about the view model or the property that is being changed. During testing, you will know and can cast the results at that time to the specified type.

We'll also need to declare a list of this new class type to hold all of the property changes that occur.

public class PropertyChangeTracker 
{
  private INotifyPropertyChanged _vmInstance;
  public List<PropertyChangedInfo> ChangedProperties;

  public PropertyChangeTracker(INotifyPropertyChanged vmInstance) 
  {
    ChangedProperties = new List<PropertyChangedInfo>();
    _vmInstance = vmInstance;
    _vmInstance.PropertyChanged += Tracker;
  }
}

Now the Tracker method can add to this list every time it's called using the new PropertyChangedInfo class.

private void Tracker(object sender, PropertyChangedEventArgs e) 
{
  var prop = sender.GetType().GetProperty(e.PropertyName);
  var value = prop?.GetValue(sender);
  var changedProp = new PropertyChangedInfo() 
  {
    PropertyName = prop?.Name,
    Value = value
  };

  ChangedProperties.Add(changedProp);
}

Using it in a test:

// Assuming a method called SaveUser that modifies a boolean property twice
var tracker = new PropertyChangeTracker(classUnderTest);
classUnderTest.SaveUser();

// Using Fluent Assertions
((bool) tracker.ChangedProperties.First().Value).Should().BeTrue();
((bool) tracker.ChangedProperties.Last().Value).Should().BeFalse();

One More Improvement...

There's one problem with this implementation. If any other properties are modified within the method, those would also be added to the list. We could get around that issue by filtering the list with PropertyName:

var newList = tracker.ChangedProperties.FindAll(x => x.PropertyName == "IsBusy");

This seems like too much work. This would require a lot of repetition constantly filtering trackers every time we use them. One of the best principles a programmer can take advantage of is DRY (Don't Repeat Yourself), and we can do that with an option to only track properties we care about. Time to improve the tracker!

We'll start by accepting an optional property name in the constructor:

private string PropertyFilterName;

public PropertyChangeTracker(INotifyPropertyChanged vmInstance, string propNameToTrack = "")
{
  PropertyFilterName = propNameToTrack;
}

Then, in the Tracker code, add a few if statements to control which property is added to the changed list.

if (PropertyFilterName != string.Empty)
{
  // Only add to the list if the names match the filter.
  if (PropertyFilterName == prop?.Name)
  {
    ChangedProperties.Add(changedProp);
  }
}
else 
{
  // if PropertyFilterName is blank, track all changes.
  ChangedProperties.Add(changedProp);
}

Done! Now this same test with a little modification will only track the property that we care about asserting.

var tracker = new PropertyChangeTracker(classUnderTest, "IsBusy");
classUnderTest.SaveUser();

((bool) tracker.ChangedProperties.First().Value).Should().BeTrue();
((bool) tracker.ChangedProperties.Last().Value).Should().BeFalse();

Wrapping Up

I've been doing a deep dive through work and my free time in C#, learning everything it has to offer. I'm sure there are better ways to do property change tracking, but this worked for my use case. I'm extremely open to improvements and changes! Comment if this helps you or you can help make this code better! Here's the full class:

using System.Collections.Generic;
using System.ComponentModel;

namespace SharedUtilities.Testing
{
    public class PropertyChangeTracker
    {
        public List<PropertyChangedInfo> ChangedProperties;
        private INotifyPropertyChanged _vmInstance;
        private string PropertyFilterName;

        public PropertyChangeTracker(INotifyPropertyChanged vmInstance, string propNameToTrack = "")
        {
            PropertyFilterName = propNameToTrack;
            ChangedProperties = new List<PropertyChangedInfo>();
            _vmInstance = vmInstance;
            _vmInstance.PropertyChanged += Tracker;
        }

        private void Tracker(object sender, PropertyChangedEventArgs e)
        {
            var prop = sender.GetType().GetProperty(e.PropertyName);
            var value = prop?.GetValue(sender);
            var changedProp = new PropertyChangedInfo()
            {
                PropertyName = prop?.Name,
                Value = value
            };

            if (PropertyFilterName != string.Empty)
            {
                if (PropertyFilterName == prop?.Name)
                {
                    ChangedProperties.Add(changedProp);
                }
            }
            else
            {
                ChangedProperties.Add(changedProp);
            }
        }
    }

    public class PropertyChangedInfo
    {
        public string PropertyName { get; set; }
        public object Value { get; set; }
    }
}

Discussion

pic
Editor guide