Implementing a Property Change Tracker For Testing in C#
Tags: csharp, dotnet, testing
Published: 2019-12-02 22:07:09

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.

```c#
// 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](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?view=netframework-4.8).

```c#
namespace SharedUtilities.Testing
{
  private INotifyPropertyChanged _vmInstance;  

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

`PropertyChangeTracker`, utilizes constructor injection to take advantage of [inversion of control](https://martinfowler.com/articles/injection.html) 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.

```c#
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.

```c#
//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.

```c#
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.

```c#
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.

```c#
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:

```c#
// 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`:

```c#
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:

```c#
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.

```c#
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.

```c#
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:

```c#
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; }
    }
}
```