Stefan Lieser

Stefan Lieser

Die kniffligen Fälle beim Testen – Events

Bei diesem Beitrag handelt es sich um eine überarbeitete Version. Er wurde zuvor bereits bei refactoring-legacy-code.net veröffentlicht.

Dies ist eine Beitragsserie. Die weiteren Beiträge finden Sie hier:

Events automatisiert Testen mit NUnit

Events spielen eine große Rolle in .NET Anwendungen. Häufig basieren große Teile der Verbindung zwischen der UI und „dem Rest“ der Anwendung auf Events. Auch außerhalb der UI gibt es viele Stellen, an denen Events sehr nützlich sind. Hier wäre vor allem das Thema asynchrone Aufrufe zu nennen. Asynchrone Aufrufe bestehen oft aus einer Methode, über die der asynchrone Vorgang gestartet wird, sowie einem Event, über den das Resultat geliefert wird, sobald es zur Verfügung steht.

Bei Events gibt es vier Themen, die beim automatisierten Testen relevant sind:

  • Prüfen, ob ein Event ausgelöst wird. Ggf. muss dabei überprüft werden, ob die gelieferten Daten den Erwartungen entsprechen.
  • Prüfen, ob ein Eventhandler erforderlich ist.
  • Prüfen, ob eine Bindung an den Event stattfindet, ob es also zu einem Subscribe kommt bzw. umgekehrt, ob die Eventbindung wieder entfernt wird (Unsubscribe).
  • Auslösen des Events im Test, um zu prüfen, ob andere Funktionseinheiten dann wie erwartet reagieren.

Prüfen, ob ein Event ausgelöst wird

Für einen automatisierten Test der überprüft, ob ein Event ausgelöst wird, soll folgende Klasse als Beispiel dienen.

public class Sut
{
    public event Action EinEreignis = delegate { };

    public event Action EinEreignisMitDaten;

    public void DoSomething() {
        EinEreignis();
    }

    public void DoSomeOtherThing(string daten) {
        EinEreignisMitDaten(daten);
    }
}

Die Klasse verfügt über zwei Methoden, die jeweils einen Event auslösen. Der Event EinEreignis wird von der DoSomething Methode ausgelöst und ist parameterlos. An diesen Event ist mit delegate { } ein leerer Eventhandler angehängt, so dass der Event immer ausgelöst werden kann. Selbst wenn der Verwender der Klasse keinen Handler mit += registriert, wird dennoch keine null Exception ausgelöst, weil immer der leere Defaulthandler angehängt ist. Für Eventhandler die optional sind, ist dies bis C# 6.0 syntaktisch die eleganteste Form.

In C# 6.0 kann das Problem durch ein Fragezeichen gelöst werden, wie folgendes Listing zeigt.

EinEreignis?.Invoke();

Durch das Fragezeichen wird erst geprüft, ob EinEreignis möglicherweise null ist. Nur wenn das nicht der Fall ist, wird die Invoke Methode aufgerufen und damit der Event ausgelöst.

Ein automatisierter Test, mit dem überprüft wird, ob der Event ausgelöst wird, sieht wie folgt aus:

[TestFixture]
public class EventTests
{
    private Sut sut;

    [SetUp]
    public void Setup() {
        sut = new Sut();
    }

    [Test]
    public void Event_wird_ausgelöst() {
        var count = 0;
        sut.EinEreignis += () => count++;

        sut.DoSomething();

        Assert.That(count, Is.EqualTo(1));
    }
}

Im Test wird eine Lambda Expression an den Event angehängt. Innerhalb dieser wird die Variable count mit jedem Auslösen des Events inkrementiert. Auf diese Weise kann nach Aufruf der DoSomething Methode geprüft werden, ob der Event genau einmal ausgelöst wurde.

Prüfen der Daten des Events

Bei einem Event, der Daten mitliefert, können diese in der Lambda Expression in eine Variable der Testmethode kopiert werden. So können sie im Anschluss an die Eventauslösung überprüft werden.

[Test]
public void Daten_des_Event_prüfen() {
    var eventDaten = "";
    sut.EinEreignisMitDaten += x => eventDaten = x;

    sut.DoSomeOtherThing("x");

    Assert.That(eventDaten, Is.EqualTo("x"));
}

In diesem Beispiel liefert der Event den String, der als Parameter der DoSomeOtherThing Methode angegeben wurde, in diesem Fall ein „x“. Im Eventhandler kopiere ich den Parameter des Events in eine Variable, die dann nach Aufruf der Methode überprüft werden kann.

Prüfen, ob ein Handler erforderlich ist

Bei Events, die optional sind, sollte ein automatisierter Test überprüfen, dass unmittelbar nach dem Instanzieren eines Objekts der Klasse keine null Exception ausgelöst wird, wenn der Event ausgelöst wird. Hier geht es also darum herauszufinden, ob der Event vor dem Auslösen auf null geprüft wird.

[Test]
public void Eventhandler_ist_optional() {
    Assert.DoesNotThrow(() => new Sut().DoSomething());
}

Der Test prüft, ob bei Aufruf von DoSomething aus dem Beispiel weiter oben keine Ausnahme ausgelöst wird.

Prüfen auf Subscribe und Unsubscribe

Manchmal ist es erforderlich zu überprüfen, ob eine Funktionseinheit sich mit einem Event verbindet (Subscribe) bzw. wieder löst (Unsubscribe). Das Binden an einen Event findet üblicherweise mit dem += Operator statt. Umgekehrt kann die Bindung mit -= wieder gelöst werden.

Das folgende Beispiel zeigt, wie die Überprüfung mit Hilfe von TypeMock Isolator [https://typemock.com] erfolgen kann.

using System;
using NUnit.Framework;
using TypeMock.ArrangeActAssert;

namespace events
{
    [TestFixture, Isolated]
    public class EventSubscriberTests_TypeMock
    {

        [Test]
        public void Subscribes_to_Event() {
            var withEvent = Isolate.Fake.Instance();
            var sut = new Sut(withEvent);

            sut.DoSomething();

            Isolate.Verify.WasCalledWithAnyArguments(
                () => withEvent.MyEvent += null);
        }

        public class WithEvent
        {
            public void DoSomething() {
                MyEvent();
            }

            public event Action MyEvent;
        }

        public class Sut
        {
            private readonly WithEvent withEvent;

            public Sut(WithEvent withEvent) {
                this.withEvent = withEvent;
            }

            public void DoSomething() {
                withEvent.MyEvent += delegate { };
            }
        }
    }
}

Die Klasse Sut ist von der Klasse WithEvent abhängig. Im Konstruktor erwartet Sut eine Instanz von WithEvent. Die DoSomething Methode bindet einen Eventhandler mit += an den Event. Und genau dieser Aufruf von += soll im Test überprüft werden. Dazu injiziere ich im Test eine Attrappe von WithEvent. Diese wird mit Hilfe von TypeMock Isolator erzeugt. Dadurch kann im Test überprüft werden, ob auf der Attrappe bestimmte Methoden aufgerufen wurden. Im Beispiel oben wird mit Isolate.Verify.WasCalledWithAnyArguments überprüft, ob die in der Lambda Expression angegebene Methode, hier der Operator +=, aufgerufen wurde. Der Parameter wird dabei ignoriert.

Ohne den Einsatz von TypeMock Isolator ist für dieses Testszenario ein anderer Weg erforderlich, um auf dem injizierten WithEvents Objekt zu prüfen, ob der += Operator aufgerufen wurde. Hierzu ist es nicht zwingend, ein Mock Framework zu verwenden, welches auf dem Profiler API aufsetzt. Diverse Open Source Mock Frameworks können ebenfalls überprüfen, ob der += Operator aufgerufen wurde.

Auslösen eines Events

Wenn eine Funktionseinheit auf einen Event reagiert, sollte ein automatisierter Test sicherstellen, dass dabei das gewünschte Verhalten eintritt. Dazu ist es oft erforderlich, im Test einen Event auszulösen.

Als Beispiel dienen mir die beiden folgenden Klassen.

public class Sut
{
    private readonly WithEvent withEvent;
    private int count;

    public Sut(WithEvent withEvent) {
        this.withEvent = withEvent;
        this.withEvent.MyEvent += () => count++;
    }

    public int Count => count;
}

public class WithEvent
{
    public event Action MyEvent;
}

Die Klasse WithEvent verfügt über einen Event, an den sich die Klasse Sut bindet. Wenn der Event ausgelöst wird, zählt Sut einen Zähler hoch. Die Klasse WithEvent verfügt über keine Möglichkeit, den Event von außen auszulösen. Dies ist in realen Klassen ebenso der Fall. Die Definition eines Events mit dem Schlüsselwort „event“ führt dazu, dass außerhalb der Klasse, in der der Event definiert ist, nur die Operationen „+=“ und „-=“ zur Verfügung stehen. Ein Aufrufen der Invoke Methode ist syntaktisch nicht möglich:

withEvent.MyEvent.Invoke();  // Syntaktisch nicht möglich

TypeMock Isolator ist erneut die Rettung. Der folgende Test zeigt, wie der Event ausgelöst werden kann.

[Test]
public void Auf_den_Event_wird_reagiert() {
    var withEvent = Isolate.Fake.Instance();
    var sut = new Sut(withEvent);
    Assert.That(sut.Count, Is.Zero);

    // withEvent.MyEvent.Invoke(); // syntaktisch nicht möglich
    Isolate.Invoke.Event(() => withEvent.MyEvent += null);

    Assert.That(sut.Count, Is.EqualTo(1));
}

Zunächst erstelle ich durch den Aufruf von Isolate.Fake.Instance() eine Attrappe vom Typ WithEvent. Diese Attrappe wird als Abhängigkeit in die Klasse Sut reingereicht. Dadurch kann der Event anschließend mit Isolate.Invoke.Event ausgelöst werden. Um mittels Lambda Expression zu definieren, welcher Event gemeint ist, wird bei TypeMock Isolator der += Operator verwendet. Hier wird also nicht null an den Event gebunden, sondern auf diesem Weg wir dem Isolator mitgeteilt, welcher Event ausgelöst werden soll.

Fazit

Der Umgang mit Events in automatisierten Tests stellt in der Regel kein großes Problem dar. In den meisten Fällen ist es damit getan, eine Lambda Expression als Eventhandler anzuhängen und dann zu prüfen, ob sie aufgerufen wird. Die komplizierteren Fälle sind wieder einfacher unter Test zu stellen, wenn das passende Werkzeug bereitsteht. Mock Frameworks können hier helfen. In diesem Fall muss es nicht einmal die „Wunderwaffe“ sein, sondern es genügt oft eines der Open Source Frameworks.

Unsere Seminare

course
Clean Code Developer Basics

Prinzipien und Tests – Das Seminar wendet sich an Softwareentwickler, die gerade beginnen, sich mit dem Thema Softwarequalität auseinanderzusetzen. Es werden die wichtigsten Prinzipien und Praktiken der Clean Code Developer Initiative vermittelt.

zum Seminar »
course
Clean Code Developer Advanced

Mit Flow Design von den Anforderungen zum Clean Code – Lernen Sie mit Flow Design einen Softwareentwicklungsprozess kennen, der Sie flüssig von den Anforderungen zum Clean Code führt.

zum Seminar »
course
Clean Code Developer Trainer

Seminare als Trainer durchführen – Dieses Seminar wendet sich an Softwareentwickler, die ihr Wissen über die Clean Code Developer Prinzipien und Praktiken bzw. über Flow Design als Trainer an andere weitergeben möchten.

zum Seminar »
course
Clean Code Developer CoWorking

Online CoWorking inkl. Coaching –
Wir werden häufig gefragt, was man als Entwickler tun könne, um kontinuierlich dran zu bleiben am Thema Clean Code Developer. Unsere Antwort: Treffen Sie sich regelmäßig wöchentlich online mit anderen Clean Code Developern.

zum Seminar »

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht.