Unit testing events
Maybe you have already been in a situation, when you needed to test public events on some tested class. Problem is, how to suspend a test for some time when event calls are expected and how to log if events were really invoked as expected.
Here is my simple solution …
* Implementations of used classes EventTester used for events unit testing and EventClass used for demonstration purposes can be found in a textual form here and here or available for downloading as a Visual Studio 2008 project.
At first let’s look at this simple class EventClass used for demonstration purposes.

This class has two events EventInt and EventString returning integer or string values. Then there are two methods InvokeEventInt() and InvokeEventString() responsible for invoking corresponding event every 0.5 second.
Returned event values are:
| method | 0.0 sec | 0.5 sec | 1.0 sec | 1.5 sec | 2.0 sec |
|---|---|---|---|---|---|
| InvokeEventInt() | 0 | 1 | 2 | 3 | 4 |
| InvokeEventString() | “value0″ | “value1″ | “value2″ | “value3″ | “value4″ |
Unit testing events using EventTester
EventTester is a class used for unit testing events. Here is an example how it is used for testing events on EventClass
[TestMethod()]
public void EventClass_EventIntTest()
{
// All events should be invoked in less then 3 seconds
int timeout = 3000;
// Those are expected event values
int[] expectedValues = { 0, 1, 2, 3, 4 };
// Event tester, logged values for tested events will be integers
EventTester<int> et = new EventTester<int>(timeout, expectedValues);
// Tested class
EventClass eventClass = new EventClass();
eventClass.EventInt += n => et.Event(n); // Bind event to EventTester
eventClass.InvokeEventInt(); // Start invoking EventInt
// At the end start event tester
et.Test();
}
You can see result of this test here

Failed test
If we change expected values to
int[] expectedValues = { 0, 1, 2, 300, 4 };
then we know that those won’t be satisfied because EventClass returns only sequence of integers from 0 to 4. Now the test result will look like this:

Detailed usage description
-
At first we have to create instance of
EventTesterclass and define what data type will be logged values. In our case EventInt event returns integers and so we set it to int. As a constructor parametersEventTesterexpects maximal time for invoking all expected events (timeout), which is important because if events are not invoked at all, then test won’t be stuck but it will fail after specified time. Second argument is expectedValues which is list of expected values returned by tested event (order matters). - Bound tested event to
et.Event(value)method - Start
et.Test()
Another example - testing different events
EventTester allows us also test different events at once and test if events were invoked in expected order. We have two options here, we can set generic data type of EventTester to object, which is most general data type in C# or we can set it to, for example, string and convert all the values to string as demonstrated in following sample:
[TestMethod()]
public void EventClass_EventInt_EventStringTest()
{
// All events should be invoked in less then 3 seconds
int timeout = 3000;
// Those are expected event values
string[] expectedValues = { "0", "value0", "1", "value1", "2",
"value2", "3", "value3", "4", "value4" };
// Event tester, logged values for tested events will be strings
EventTester<string> et = new EventTester<string>(timeout, expectedValues);
// Tested class
EventClass eventClass = new EventClass();
eventClass.EventInt += n => et.Event(n.ToString());
eventClass.EventString += s => et.Event(s);
eventClass.InvokeEventInt(); // Start invoking "EventInt" event
System.Threading.Thread.Sleep(50);
eventClass.InvokeEventString(); // Start invoking "EventString" event
// At the end start event tester
et.Test();
}
And result:

Execution of eventClass.InvokeIntString() method is 50 milliseconds delayed to guarantee that EventInt event will be invoked always before InvokeString event.
Implementation
Here is implementation of EventTester class used for unit testing events:
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests
{
class EventTester<T>
{
int _timeout; // Timeout, after this time is logging stopped
bool _running; // Inidicates if logging is already running
int _startTime; // Time when logging started
List<T> _expectedEvents; // Expected invoked events
List<T> _loggedEvents; // Logged invoked events
/// <summary>
/// Test if events were invoked as expected
/// </summary>
/// <param name="timeout">Timeout in milliseconds</param>
public EventTester(int timeout, IEnumerable<T> exptectedEvents)
{
_timeout = timeout; // Set timeout value
_running = false;
_expectedEvents = new List<T>(exptectedEvents);
_loggedEvents = new List<T>();
}
/// <summary>
/// This function should be bound to tested events
/// </summary>
/// <param name="event_">Logged value returned from event</param>
public void Event(T event_)
{
_loggedEvents.Add(event_);
}
/// <summary>
/// Compare two Lists if they are the same
/// </summary>
/// <param name="list1">First list</param>
/// <param name="list2">Second list</param>
/// <returns>
/// Returns true if lists are same,
/// else returns false
/// </returns>
/// <remarks>Compares particular elements of lists</remarks>
protected bool compare(IEnumerable<T> list1, IEnumerable<T> list2)
{
// Number of elements is different so lists are different
if (list1.Count() != list2.Count()) return false;
// Iterate thru all elements
for (int i = 0; i < list1.Count(); i++)
if (!list1.ElementAt<T>(i).Equals(list2.ElementAt<T>(i)))
return false; // Lists are different
return true; // Lists are same
}
/// <summary>
/// Start test
/// </summary>
public void Test()
{
_running = true;
_startTime = Environment.TickCount; // Set time when logging started
// Wait until all events are called as expected or timeout
while (_running)
{
// forces immediate evaluation of _loggedEvents list
// (some kind of LINQ lazy evaluation causes problems here,
// .ToList() causes immediate evaluation)
_loggedEvents.ToList();
// Are logged event values same as expected?
bool eq = compare(_loggedEvents,
_expectedEvents.GetRange(0, _loggedEvents.Count));
// Successful test, lists are equal
if (eq && (_loggedEvents.Count == _expectedEvents.Count))
{
_running = false;
}
// Last event was not expected
else if (!eq)
{
// just create string of logged event values separated by comma
string logged = List.Foldr<T, string>(
_loggedEvents, "", (x, y) => string.Format("{0},{1}", x, y));
// just create string of expected event values separated by comma
string expected = List.Foldr<T, string>(
_expectedEvents.GetRange(0, _loggedEvents.Count), "",
(x, y) => string.Format("{0},{1}", x, y));
// call test fail
Assert.Fail(@"Last called event was not expected.
Called: {0} Expected: {1}", logged, expected);
break;
}
// Timeout
else if (Environment.TickCount > _startTime + _timeout)
{
// just create string of logged event values separated by comma
string logged = List.Foldr<T, string>(
_loggedEvents, "", (x, y) => string.Format("{0},{1}", x, y));
// just create string of expected event values separated by comma
string expected = List.Foldr<T, string>(
_expectedEvents, "", (x, y) => string.Format("{0},{1}", x, y));
// call test fail
Assert.Inconclusive(@"Event tester timeout ‘{0}’ milliseconds;
Logged: ‘{1}’ Expected: ‘{2}’", _timeout, logged, expected);
break;
}
// wait for 10 milliseconds and test again
System.Threading.Thread.Sleep(10);
}
}
}
}
Here is implementation of EventClass:
using System;
using System.Threading;
namespace EventTesterAndDemo
{
// Demo class with two events
public class EventClass
{
Thread _thread;
// Delegates
public delegate void EventStringHandler(string value);
public delegate void EventIntHandler(int value);
// Events
public event EventStringHandler EventString;
public event EventIntHandler EventInt;
// Contructor
public EventClass() { }
// Invoke EventInt with values 0, 1, 2, 3 and 4
public void InvokeEventInt()
{
// Call events
_thread = new Thread(x =>
{
for (int i = 0; i < 5; i++)
{
// Call event
if (EventInt != null) EventInt(i);
// Wait for 0.5 second
Thread.Sleep(500);
}
});
_thread.Start();
}
// Invoke EventString with values "value0", "value1",
// "value2", "value3" and "value4"
public void InvokeEventString()
{
// Call events
_thread = new Thread(x =>
{
for (int i = 0; i < 5; i++)
{
// Call event
if (EventInt != null)
EventString("value" + i.ToString());
// Wait for 0.5 second
Thread.Sleep(500);
}
});
_thread.Start();
}
}
}
Download code

