Unit testy událostí
Možná jste již byli v situaci, kdy jste potřebovali veřejnou událost (či události) na nějaké třídě testovat unit testy. Problém je, jak pozastavit test na určitou dobu kdy jsou očekávána volání patřičné události a také jak tyto volání logovat, aby unit test mohl porovnat zda všechna volání proběhla s očekávanými hodnotami a pořadí.
Zde je mé jednoduché řešení…
* Implementaci tříd EventTester použité k testování událostí a EventClass používané pro demonstrační učely v tomto článku, můžete najít na konci tohoto článku v textové podobě zde a zde nebo ke stažení jako Visual Studio 2008 projekt.
Nejdříve se podívejme na jednoduchou třídu EventClass na které budeme testovat události:

Tato třída má dvě události EventInt a EventString vracející integer nebo string hodnoty. Pak zde máme dvě metody InvokeEventInt() a InvokeEventString(), které vyvolá patřičnou událost každých 0.5 sekund.
Vrácené hodnoty událostí vypadají takto:
| metoda | 0.0 sek | 0.5 sek | 1.0 sek | 1.5 sek | 2.0 sek |
|---|---|---|---|---|---|
| InvokeEventInt() | 0 | 1 | 2 | 3 | 4 |
| InvokeEventString() | “value0″ | “value1″ | “value2″ | “value3″ | “value4″ |
Unit testy s použitím třídy EventTester
EventTester je třída sloužící pro unit testování událostí. Zde je příklad testování události na objektu třídy EventClass s použitím EventTester:
[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();
}
Výsledek testu:

Neúspěšný test
Pokud změníme očekávané hodnoty na
int[] expectedValues = { 0, 1, 2, 300, 4 };
…víme, že tyto hodnoty nebudou vráceny událostí, protože ta vrací pouze celá čísla od 0 do 4. Nyní výsledek testu vypadá následovně:

Používání třídy EventTester
-
Nejdříve musíme vytvořit instanci třídy
EventTesterkde musíme definovat datový typ zaznamenaných hodnot události. V našem případě událost EventInt vrací celá čísla a proto nastavíme datový typ na int.EventTesterkonstruktor má dva parametry, prvním je očekávaná doba, po kterou bude testovaná událost volána (timeout), což je důležité v případě, že by událost nebyla volána vůbec, pak test nebude pouze neustále čekat na hodnotu, ale zastaví se po určité době jako neúspěšný unit test. Druhý parametr je expectedValues což je pole očekávaných hodnot vrácených událostí (na pořadí hodnot záleží). - Volání metody
et.Event(value)včetně předaného parametru kdykoliv je testovaná událost vyvolána - Spustit test
et.Test()
Další příklad - testování různých událostí
EventTester může testovat i více událostí najednou a testovat tak, zda události byly také volány v očekávaném pořadí.
Pro testování různých událostí máme dvě možnosti, můžeme nastavit generický typ třídy EventTester na object, což je nejobecnější typ v C# nebo můžeme nastavit datový typ na, například, string a zkonvertovat všechny hodnoty právě na string stejně jako v následujícím příkladu:
[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();
}
Výsledek obou testů (včetně předchozího):

Volání eventClass.InvokeIntString() metody je spožděna o 50 milisekund, aby událost EventInt byla volána vždy před událostí InvokeString.
Implementace
Zde je implementace třídy EventTester používané pro testování událostí:
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);
}
}
}
}
Zde je implementace třídy 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();
}
}
}
Stáhnout
Stáhnout projekt EventTester včetně demo ukázky můžete zde:

