Unit testing event handlers

D

David Veeneman

This post is for the benefit of the Google spider and needs no response.

How do you unit test an event handler with NUnit? An event handler is not a
test method, and Asserts in test class event handlers will not be tested.
So, if the test class is to contain event handlers, the most obvious
solution is to return values in the event args and test those results in the
test method.

But that approach couples the event to the test framework. A better
alternative is to use an anonymous delegate:

[Test]
public void TestOrderDispatchC()
{
// Create order
Order testOrder = new Order();
OrderItem testItem1 = testOrder.AddOrderItem();
testItem1.Status = ItemStatus.Undelivered;
OrderItem testItem2 = testOrder.AddOrderItem();
testItem2.Status = ItemStatus.Undelivered;

// Create anonymous delegate
DeliveryNeededEventHandler anonymousDelegate = delegate(object
sender, DeliveryNeededEventArgs e)
{
Assert.AreEqual(2, e.OrderItems.Count);
};

// Subscribe to DeliveryNeeded event
testOrder.DeliveryNeeded += anonymousDelegate;

// Dispatch order
testOrder.DispatchOrder();

// Unsubscribe from the DeliveryNeeded event
testOrder.DeliveryNeeded -= anonymousDelegate;
}

Be sure to unsubscribe from the event at the end of the method. Failing to
unsubscribe causes a memory leak (yes, you can get them in .NET). The memory
leak occurs with any delegate, not just anonymous delegates, if you don't
unsubscribe.

David Veeneman
Foresight Systems
 
M

Marc Gravell

This post is for the benefit of the Google spider

I'm sure it is grateful...
Be sure to unsubscribe from the event at the end of the method. Failing to
unsubscribe causes a memory leak (yes, you can get them in .NET). The memory
leak occurs with any delegate, not just anonymous delegates, if you don't
unsubscribe.

Once testOrder goes out of scope it is eligible for collection, as is
the delegate assuming you haven't shared it anywhere.
If you *had* to unsubscribe you should be using "finally" anyway...

Marc
 
D

David Veeneman

Nope--the garbage collector won't collect it if you haven't unsubscribed.
Ask the Princeton team for the DARPA Grand Challenge:

http://www.codeproject.com/showcase/IfOnlyWedUsedANTSProfiler.asp

A memory leak of this exact type torpedoed their entry in DARPA's $2 million
Grand Challenge. So, yes, you do have to unsubscribe.

But the suggestion about 'finally' is a good one.

David Veeneman
Foresight Systems
 
J

Jon Skeet [C# MVP]

Nope--the garbage collector won't collect it if you haven't unsubscribed.

Yes, it will.
Ask the Princeton team for the DARPA Grand Challenge:

http://www.codeproject.com/showcase/IfOnlyWedUsedANTSProfiler.asp

Yes, you can get garbage collection issues if you don't unsubscribe
from events - but not the way you're suggesting.
A memory leak of this exact type torpedoed their entry in DARPA's $2 million
Grand Challenge. So, yes, you do have to unsubscribe.

No, you don't.

You need to understand which way the references work. A delegate
instance holds a reference to its target - the object it will call the
method on.

Suppose you have a form, and some random expensive object subscribes
to the Click event of a button on that form: the random expensive
object won't be eligible for garbage collection until either the form
itself is eligible for garbage collection, or the event handler is
unsubscribed - the form effectively has a reference to the object. The
reverse is *not* true, however: the form can become eligible for
garbage collection *without* the random expensive object becoming
eligible.

So to cut it short: no, in the unit test example you gave, you do
*not* need to unsubscribe.

Jon
 
M

Marc Gravell

Different problem; they had a long-lived object (the car) and lots of
short-lived objects (the obstacles). The obstacles subscribed and
forgot to unsubscribe. However! If the car had been collected, so
would have *all* the obstacles. In your scenario you have no long-
lived object.

Big difference. But yes, if short-lived objects don't unsubscribe, a
long-lived object can keep them alive.

Marc
 
D

DeveloperX

Nope--the garbage collector won't collect it if you haven't unsubscribed.
Ask the Princeton team for the DARPA Grand Challenge:

http://www.codeproject.com/showcase/IfOnlyWedUsedANTSProfiler.asp

A memory leak of this exact type torpedoed their entry in DARPA's $2 million
Grand Challenge. So, yes, you do have to unsubscribe.

But the suggestion about 'finally' is a good one.

David Veeneman
Foresight Systems

The following code would suggest you're wrong. Two buttons, the first
creates two objects and subscribes to their events. The first object
goes out of scope, the second object is assigned to a variable.
The second button just does a GC.Collect(), which encourages
collection and triggers the finalizers. So you should see in the
console:

Added
Added
Destroyed

Of course this assumes I understand the problem correctly which is
sometimes hit and miss when dealing with this topic.


using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace WindowsApplication31
{
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.ComponentModel.Container components = null;
private Test _test;

public Form1()
{
InitializeComponent();
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.SuspendLayout();
this.button1.Location = new System.Drawing.Point(16, 8);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(48, 40);
this.button1.TabIndex = 0;
this.button1.Text = "button1";
this.button1.Click += new System.EventHandler(this.button1_Click);
this.button2.Location = new System.Drawing.Point(168, 16);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(40, 40);
this.button2.TabIndex = 1;
this.button2.Text = "button2";
this.button2.Click += new System.EventHandler(this.button2_Click);
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.Add(this.button2);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
}
#endregion
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
private void button1_Click(object sender, System.EventArgs e)
{
TestDisconnect d = new TestDisconnect();
d.DoTest();
TestDisconnect d2 = new TestDisconnect();
_test = d2.DoTest();
}
private void button2_Click(object sender, System.EventArgs e)
{
GC.Collect();
}
}
public class TestDisconnect
{
public Test DoTest()
{
Test t = new Test();
t.TestIt+=new TestEventHandler(t_TestIt);
return t;
}
private void t_TestIt(object sender, EventArgs e)
{
Console.WriteLine("Fired");
}
}
public delegate void TestEventHandler(object sender, EventArgs e);
public class Test
{

private event TestEventHandler _testIt;
public event TestEventHandler TestIt
{
add
{
Console.WriteLine("Added");
_testIt+=value;
}
remove
{
Console.WriteLine("removed");
_testIt-=value;
}
}
~Test()
{
Console.WriteLine("Destroyed");
}
}
}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top