Unit testy událostí

go to english version »

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

  1. Nejdříve musíme vytvořit instanci třídy EventTester kde 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. EventTester konstruktor 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ží).
  2. Volání metody et.Event(value) včetně předaného parametru kdykoliv je testovaná událost vyvolána
  3. 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:

Leave a Reply