Here's a sample test (pay attention to the "tracker" variable):
The "WaitForChange" method takes 2 parameters: the property name we're checking and a timeout (in seconds). Before going into the code, let's see why this may be important.
Articles in this Series
o Tracking Property Changes in Unit Tests (this article)
o Update 1: Supporting "All Properties"
o Update 2: Finer-Grained Timeout
o Update 3: Async Tests
INotifyPropertyChanged and Asynchronous Methods
I've spent a lot of my time in view models. These are just classes that have properties that we data bind to our UI elements and methods that can call into the rest of our application.
In order for data binding to work as expected, we implement the "INotifyPropertyChanged" interface. This interface has a single member (an event), but we usually set up a helper method that make it easy to raise this event.
Here's a common implementation of INotifyPropertyChanged:
This code is a little bit different than code I've shown before. Thanks to Brian Lagunas for pointing out that the we should add the intermediate "handler" variable to take care of potential multithreading issues. I haven't run into these issues myself, but I'm sure I will as our UIs get more asynchronous and parallel.
Then in the setters for our properties, we call "RaisePropertyChanged":
So now, whenever the "LastUpdateTime" property is set (and the value is different from the current value), it will raise the PropertyChanged event, and this will notify the affected UI elements that they need to update their values.
Note: I would normally recommend using the CallerMemberName attribute here, but this code is taken from a .NET 4.0 application, so that attribute is not available.
Asynchronous Method Calls
Normally, we don't need to know when a property changed in our unit tests. The adventure comes when we have asynchronous method calls. If we have an asynchronous method call that updates a property when it completed, we can tap into the INotifyPropertyChanged event rather than setting up full interaction with the asynchronous method.
This is especially true when dealing with the Asynchronous Programming Model (APM) which this particular code sample uses. This async approach has been around for a long time (and thankfully has been mostly replaced by TAP (Task Asynchronous Pattern) with Task, async, and await).
Here's our asynchronous method:
I won't go into the details here. This takes a service call that uses APM (the "_service" object with "BeginGetPeople" and "EndGetPeople") and switches it to TAP (creating a Task and a continuation).
The important part that we're looking for here is that the "LastUpdateTime" is set in the continuation. That means we can use this as a signal that the continuation has completed.
Tests with the Asynchronous Call
I'm a bit torn on whether it's best to look at the test in detail or to look at the "PropertyChangeTracker" first. Let's start with the test:
This test checks to see if the client-side cache is working. This application has a 10 second cache (for demonstration purposes). If the cache is still valid, then the catalog service should *not* be called. But if the cache is expired, then the service *should* be called. This test checks that the service is called again when the cache has expired.
Here's the progression of this test:
- The view model (CatalogViewModel) is created. This is the class that we're testing.
- The tracker (PropertyChangeTracker) is created.
- The "Initialize" method is called on the view model. This calls the "RefreshCatalogFromService" method that we saw above. This has asynchronous behavior.
- The "WaitForChange" method waits for the "LastUpdateTime" to be changed by the asynchronous method. This signals that the asynchronous call is complete and it is safe to continue.
- The "LastUpdateTime" is set to an hour in the past. This sets the cache to expired.
- The "Reset" method is called on the tracker.
- The "RefreshCatalog"method is called on the view model. We expect that this *will* call the "RefreshCatalogFromService" method.
- The "WaitForChange" method is called again on "LastUpdateTime".
- The assert will check that the method on our mock service was called 2 times. (This mock setup is hidden in this case (which isn't good). It would be better if we got rid of the setup method for these tests).
By waiting for the "LastUpdateTime" property to be updated, we know that the asynchronous process has completed, and it is safe to continue with our tests. We can think of this as a "block until changed or timeout".
Let's look at the code that makes this possible.
PropertyChangeTracker in Prism 4
I borrowed the PropertyChangeTracker class from the Prism 4 test helpers. Here's the code for that class (notice that "WaitForChange is not part of this class).
I ran across this class when I was working with Prism 4 a few years back (here and here). I thought it was pretty clever and useful. Here's what it does in its "raw" state.
The constructor takes a parameter of a class that implements that "INotifyPropertyChanged" interface. Then it hooks up it's own event handler to the "PropertyChanged" event. In this case, it will add the name of the property to a private "notifications" collection.
The only public method on this class is "Reset" which will clear out the notification list. The "ChangedProperties" property lets us get an array of the property names so that we can see if a particular one was updated.
Note: I haven't found an equivalent object in the latest code for Prism 6. I'll need to ping Brian Lagunas about that to see if there is something similar in the test suite.
[Update 07/06/2015: Per Brian, this class was no longer needed for the Prism test suite, so it was removed.]
New Functionality
This class was used as a helper as part of the Prism tests. But I wanted to use this same technique to get a little more functionality. Rather than just getting a list of the properties that have changed, I wanted to be notified *when* a property was changed.
Most of my class is the same as the Prism class:
I ended up removing the private "changer" field because it isn't used anywhere else in the class. The important bit that I added is the "WaitForChange" method:
This method takes 2 parameters: the property name and a timeout (in seconds). It uses a (very inefficient) "while" loop to keep checking whether the property name is showing up in the "notifications" collection.
The loop will continue until the property name shows up in the list or the timeout expires. The method returns "true" if the property was changed; otherwise "false".
Based on this information, it should be more apparent how the previous test works (I won't repeat it here, you can scroll up an re-read the procedure.)
Why Have a Timeout?
So the question you might have is why do we have the "maxWaitSeconds" parameter (a timeout)? It's so we can write tests like this:
This test is similar to the previous test, but it is testing the inverse state. If the cache is still valid, then the catalog service should *not* be called again. It should only be called once when the view model is initialized.
And this is why we need the timeout.
Our "Arrange" section is just the same as the previous test. But our "Act" section is different. Since we are not expiring the cache (by setting the "LastUpdateTime"), the cache should still be valid in this case. That means that when we call "RefreshCatalog", the service should *not* be called. And as a byproduct of that, the "LastUpdateTime" property is *not* updated.
So when we call "WaitForChange" the second time, we expect that the "LastUpdateTime" property will *not* be updated. So this method will not return until the timeout has expired. But it will return; it will not simply hang.
Concerns for PropertyChangeTracker
There are a few things we should look at with PropertyChangeTracker, including cases it currently misses.
Missed "All" Property Changed
One scenario that this class misses is when all of the properties are changed with a single call. If we want, we can call "RaisePropertyChanged" with a null value for the parameter. The effect of this is that the UI will rebind all of the properties.
But in our case, "WaitForChange" would return false. This would be a fairly easy fix; we just need to change the "while" condition to check for the parameter name or a "null" to denote "all properties".
The "while" Loop
The "while" loop in our method is a blocking call (it needs to be blocking because we want our test to also be "blocked"). But this has the advantage of notifying us immediately when the property is changed.
So even though this is inefficient, it works for what we need here. I have a feeling that there is a better way of implementing this immediate notification; one way may be by setting up something in the event handler. It will be interesting to explore further.
Finer-Grained Timeout
One good change would be to allow for a finer-grained timeout parameter. Right now, it is set to seconds (and whole seconds since it is an integer). It may be better to set this milliseconds or maybe event a TimeSpan. This would let us use increments smaller than 1 second.
Implementing IDisposable
A practice that we're ignoring here would be to implement the IDisposable interface on this class. Why would we want to? Because we're hooking up an event handler.
When we hook up event handlers, we create references between our objects. And because of this, we may keep the garbage collector from collecting objects that are no longer used. So generally, we like to unhook our event handlers in a Dispose method to clean things up a bit.
But we don't need to worry about that here. Our objects are extremely short-lived. In fact, they only live for the length of the individual test method. We create both objects (the view model that is being tracked, and the tracker object itself) in the test method, and they will both go out of scope when the method exits.
Because of this, we don't need to be worried about these references.
Helper Class
This is really a helper class for our tests. As such, we usually just give it the smallest amount of functionality that it needs to be useful. As we run into different scenarios, then we can expand the class a bit. But generally, if it's working for what we need, then we leave it alone.
There are a lot of interesting techniques out there. For people who have lots of view models and they need to track changes to properties, this little change tracker may be a good option.
Happy Coding!
No comments:
Post a Comment