I was recently developing a new dialog for one of our integration layers using TDD. When I do this, I use the MVC pattern. I create an interface that defines the operations available on the view, and I create a separate controller class that accepts a view in it’s constructor. I make the test fixture implement the view interface, and the actual view implementation(form) is nothing more than wiring up the interface’s implementation to it’s own controls and/or events. It’s pretty simple, and has served me well in the past.
While developing this particular view/controller, I tried a new twist. I exposed a series of events on the interface, and implemented listeners on the controller. An example would be the OnOk event, which is fired when the user clicks the Ok button on the physical dialog. The view implementation handles it’s own internal ok button click event by raising the interface’s OnOk event.
Part of the contract with the controller is that each public event on the view will be handled, and automatically wired during the construction of the controller. I wanted to force this behavior through a unit test, and came up with the following method:
void AssertIsListening( object instance , MulticastDelegate @event , string eventName );
This method takes an object instance on which the assertions will be performed, the actual event to check, and the name of the event. It is called in the form AssertIsListening( controller , OnOk , "OnOk" );
The code works by accepting a multicast delegate. The MulticastDelegate class defines a method named Delegate[] GetInvocationList();. Internally, the AssertIsListening method calls MulticastDelegate.GetInvocationList, and loops through the resulting array of delegates. During each iteration, it checks to see if the delegate’s Target member is the same as the instance method argument( instance.Equals( delegate.Target ); ). If the instance argument is not found in the list of delegate targets, the assertion fails.
Here’s the full code for the method(please forgive the formatting):
void AssertIsListening( object instance , MulticastDelegate @event , string eventName )
{
bool found = true;
Assert.IsNotNull( @event , "Event {0} is null" , eventName );
foreach( Delegate d in @event.GetInvocationList() )
{
if( instance.Equals( d.Target ) )
{
found = true;
break;
}
} //END delegate target loop
Assert.IsTrue( found , "Object not found in invocation list of event {0}" , eventName );
} //END AssertIsListening method
Using the above method, I can make sure that the controller handles each event defined on the view’s interface, and that the event handlers are added automatically from the controller’s constructor. It works well. However, when I got down to testing, I ran into an unforeseen problem: stale object references.
It’s more of a peculiarity to my test fixture than anything else, but it did expose a potential problem in the future. My test fixture has something on the order of 40 test cases. Each test case begins by calling the constructor of the controller. That means when all tests in the fixture have completed, about 40 instances of the controller have been created, which means that the mock view(the test fixture) will have 40 event listeners for each of it’s events. That means that when I fire an event from the mock view, more than once controller instance will handle the event.
When I first encountered this problem, it didn’t make any sense. After all, the controller that was being created in each test case was local to that test case. It shouldn’t have remained alive. So I threw in a GC.Collect() inside the fixture’s TearDown method to force a collection after each test case. After that I still had old controller instances handling each event. It had me completely stumped. The old controller instances should have zero references and should have been collected. What was going on?
Then I realized the problem: the test fixture instance was being reused for all test cases(NUnit 2.2). The controller automatically wires up the view’s events during construction, so each controller instance, even though local to each test case, was still “wired in” to the test fixture instance. That means that even after a garbage collection, the stale controller instances were still visible through the object reference walking performed by the GC. I solved this issue by implementing IDisposable on the controller and having the controller remove the event handlers during IDispose.Dispose();.
It seems obvious in retrospect, but it causes me concern for the future. In standard WinForms development, most event handlers are implemented within the form that exposes the events, so the delegate target object reference is a circular reference to the form itself, which the garbage collector will take care of without problem. But, there is a possibility that outside of WinForms, some component will expose an event, and the object that handles the event will never be reclaimed if the event sink is long-lived. Just a heads-up.