Friday, February 19, 2016

Tracking Properties in Unit Tests - Update 1: Supporting "All Properties"

When creating unit tests, we come across blocks of code that appear to be difficult to test. But with some thought, we can come up with some interesting solutions. Previously, we took a look at a test helper class that allows us to track changes to properties.

I've found that this code is especially helpful when testing view models that use asynchronous method calls. For more information on this scenario, please see the prior article: Tracking Property Changes in Unit Tests.

Articles in this Series
o Tracking Property Changes in Unit Tests
o Update 1: Supporting "All Properties" (this article)
o Update 2: Finer-Grained Timeout
o Update 3: Async Tests

There were some shortcomings in the original implementation. The code worked just fine in the environment that I created it for, but it falls down once we start trying to use it as a more general tool. Now it's time to address those shortcomings.

To make sure that the changes would work in various scenarios, I created a stand-alone project and a set of unit tests. The code is available on GitHub: jeremybytes/property-change-tracker.

Here are the shortfalls of the original code (as described in the prior article):
  • Missed "All" Property Changed
  • The "while" Loop
  • Finer-Grained Timeout
  • Implementing IDisposable
Today we'll look at the first item: accommodating "all properties". And we'll also look at the initial tests that go along with this.

Note: The completed code for this article is in the "All Properties" branch of the GitHub project. (It has also been merged into the master branch with some other updates.)

Single Property vs. "All" Properties
When we implement the "INotifyPropertyChanged" interface, our goal is to get the "PropertyChanged" event to fire whenever we update one of our properties. This lets the UI know that the property has been updated, so the data-bound controls will re-bind their values.

For more information on this, check out the previous article that shows different ways to implement this code: CallerMemberName vs. nameof() in INotifyPropertyChanged.

In these examples, we have been passing in a specific property name, but we can also signal that *all* properties have been changed. Here's the documentation for the PropertyChanged event:

So we can pass "null" or "String.Empty" to fire the PropertyChanged event for all properties.

The Problem
The problem is that our PropertyChangeTracker code does not currently support "all properties". Here's the original code:

We have a "notifications" list that holds the names of the properties that have been changed. Then in our "WaitForChange" method, we look for the name of a property. But if "all properties" have been changed, then the string for the specific property will not appear in this list.

Test Classes & Unit Tests
Before making these changes, we'll set up some test classes and tests. Since there are multiple ways of implementing "INotifyPropertyChanged" (as we've seen), I decided to create 3 different test classes. This is technically not necessary because we've seen that the code gets compiled down to the same IL. But I initially approached this from a naive standpoint, so I wanted to "cover the bases".

Standard Properties Class
Inside the "TestHelpers.Tests" project is a "FakeClassStandardProperties" class. This uses quoted strings in the calls to RaisePropertyChanged. Here's a snippet with a sample property:

Then there is also a public method to update "all properties":

The "RaisePropertyChanged" method has a default value for the parameter. If no value is passed in, then "null" is used -- indicating that "all properties" have been changed.

Standard Properties Tests
To test our PropertyChangeTracker, we set up 3 initial tests. First, we look at a single property:

In the first test, we initialize our fake class and then hook up the PropertyChangeTracker. Then we change the "LastName" property and ask our tracker to wait for the "LastName" property to be changed. Since we just changed the property, the tracker will have it in its list of changed items, so it will return "true" immediately.

This scenario is a bit different than described in the original article on the change tracker because we are dealing with synchronous code. We will want to add tests in the future to test with asynchronous code since that's the primary reason for using this test helper. But for now, we'll stick with basic functionality.

In the second test, we initialize our fake class and hook up the tracker, but we do *not* change the property. This means that our "WaitForChange" method will wait for 1 second and then return "false" (meaning the property was not changed within the expected timeframe).

Note: One of our shortfalls of this object is that we have a timeout measured in seconds. This will be addressed in a future update.

Testing "All Properties"
In addition to these 2 tests, we have a third to test "all properties":

This has the same setup, but instead of updating a single property, we call the "NotifyAllProperties" method on our test class.

With our code in its initial state, this test will fail.

Testing with the CallerMemberName Attribute
We have another test class that implements INotifyPropertyChanged using the CallerMemberName attribute. This class is called "FakeClassCallerMemberName". Here's a sample property:

We don't have a quoted string when we call "RaisePropertyChanged" beause we are relying on the compiler to pass "FirstName" into the method. More information on the "CallerMemberName" attribute is available in a prior article: "Using the CallerMemberName Attribute for Better XAML Data Binding".

The method to update "all properties" is a little different:

Since we are using the "CallerMemberName" attribute, we don't want to leave the parameter empty. This would pass in "NotifyAllProperties" as the parameter, which is definitely not what we want.

Instead, we pass in an explicit "null". The "null" will then be passed through to the PropertyChanged event.

Unit Tests with the CallerMemberName fake class
The tests that go along with this fake class are pretty much the same as what we saw with the standard property fake class. So, we won't look at them here. Again, the test that uses the "NotifyAllProperties" method fails.

Testing with the "nameof()" Expression
As you might imagine, there is a third test class that implements INotifyPropertyChanged using the "nameof()" expression. Here's a sample property:

And a method to update "all properties":

Notice that this is the same as the class with standard properties. That's because we "RaisePropertyChanged" has a default value for the parameter, and since we do *not* have the CallerMemberName attribute, we can go back to relying on the default to pass in the "null" value.

Unit Tests with "nameof()"
Again, the unit tests that use this class are pretty much the same as the other tests, so we won't go through them here. As above, the test that uses "NotifyAllProperties" currently fails.

Adding Support for "All Properties"
So we have our tests in place. With our original code, we do get some failures:

This shows us that the tests which call the single properties pass, but the tests which update "all properties" fail.

The reason that the failing tests take 1 second to complete is because they wait for the timeout before returning "false". (Again, we'll address this timeout in a future article.)

Updated Code
I took the simplest path in order to add support for "all properties". I made changes to the PropertyChangeTracker in 2 locations. The first was the constructor:

If we compare this to the original constructor above, we see that there we added a conditional to check to see if the parameter is null or empty. This will handle both a "null" parameter or a "String.Empty" parameter, so this matches the specifications of the PropertyChanged event that we saw above.

As a simple path, I added a magic string to our notifications list: "***ALL***". I'm not a big fan of magic strings, but this code is pretty compact and isolated.

So let's look at the other part of the code: "WaitForChange":

In this code, we have added to the condition of the "while" loop. Instead of just checking for the property name, we also check for "***ALL***".

With this code in place, all of our tests now pass:

I mentioned that I'm not a fan of magic strings, so why did I use one? Wouldn't it be better to check for the "all" parameter directly in the notifications list?

The answer is maybe. That's the code that I started with. The constructor would be unchanged from the original, but the "while" conditional looks like this:

With this code in place, all of the tests pass. But I wasn't happy with the readability. This is really a matter of opinion, so I won't recommend one over the other. But I have a preference for the version with the magic string because it's more readable. The class itself is small (only 45 lines of code), and the magic string is isolated (meaning, it doesn't cross class boundaries). So I'm willing to compromise in one area (magic string) to make another area better (readability).

Again, the code for this update is available in the "All Properties" branch on GitHub: jeremybytes/property-change-tracker.

Wrap Up
There was a bit of setup to get our tests in place. But once that was done, it was pretty easy to add our first improvement to the "PropertyChangeTracker" class. Next time around, we'll look at fixing our timeout so that we can have finer-grained values. One second is a really long minimum value, particularly if we have multiple tests that hit that timeout.

Also in the future, we'll look at adding tests that explicitly test our tracker with asynchronous code. This is closer to the real-world use for this test helper. And we may look at potential threading issues as well.

When we're looking to add features to our code, it's important to have tests in place to make sure that we don't break any existing functionality. This let's us move forward confidently and gives us immediate feedback when something goes wrong. And that makes me a faster coder.

Happy Coding!

No comments:

Post a Comment