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

  1. At first we have to create instance of EventTester class 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 parameters EventTester expects 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).
  2. Bound tested event to et.Event(value) method
  3. 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

Leave a Reply