Interfaces für bessere Unittests

In diesem Blogeintrag möchte ich dir anhand eines einfachen Beispiels zeigen, wie Du Interfaces in C# gezielt einsetzten kannst, um das unabhängige Testen einzelner Klassen in deinen C#-Projekten zu ermöglichen. Das hierfür verwendete Beispiel baut auf meinem letzten Beitrag „Einstieg ins Unit-Testen in C# – Setup, Basics und Hilfreiche Tools“ auf.

Warum sollten Unittests unabhängig von der Klasse ausgeführt werden?

Der Sinn von Unittests ist es einzelne Codeabschnitte unabhängig voneinander auf ihre Funktionalität zu prüfen. Es kann jedoch vorkommen, dass Klassen oder Methoden von einer anderen Klasse abhängen. In diesen Fällen lassen sich die Codeabschnitte nicht unabhängig voneinander Testen. Besonders problematisch ist dies, wenn die Klasse, von der unser zu testender Abschnitt abhängt, noch nicht vollständig getestet ist, fehlerhaft ist oder vielleicht noch gar nicht implementiert wurde.

Im folgenden Beispiel wird gezeigt, wie das NuGet Package „FakeItEasy“ und Interfaces genutzt werden können, um Klasse dennoch unabhängig testen zu können.

 

Beispiel – Die zu testende Klasse ist abhängig von einer anderen Klasse

In diesem Beispiel welchen aus dem Blogeintrag: „Einstieg ins Unit-Testen in C# – Setup, Basics und Hilfreiche Tools“ aufbaut, haben wir unsere zu testende Klasse Calculator (Abbildung 1). Wir möchten die Methode Calc(int n1, int n2, operation operation) für den Fall einer Addition testen. Calc(int n1, int n2, operation operation) ruft eine Methode einer Instance der Klasse Addition (Abbildung 2) auf welche fehlerhaft implementiert ist, aufgrund dessen gibt unsere Methode Calc(int n1, int n2, operation operation) einen Falschen Wert zurückgibt.

Der Unittest aus Abbildung 3 welcher die Methode Calc(int n1, int n2, operation operation) testet hängt zusätzlich von der Klasse Addition ab und nicht ausschließlich von unserer Klasse Calculator.


Abbildung 1 Die zu testende Klasse Calculator besitzt ein Objekt der Klasse Addition und verwendet dieses in der Methode Calc zum Berechnen des Ergebnisses


Abbildung 2 Die Klasse Addition, von welcher unsere Klasse Calculator abhängt, ist fehlerhaft implementiert und gibt als Rückgabewert für jeden Aufruf der Methode Add(int n1, int n2) die Zahl 0 zurück


Abbildung 3 Der Unittests zur Testung der Methode Calc(int n1, int n2, operation operation) wird nicht bestanden, weil die Klasse Addition das falsche Ergebnis zurückgibt

Der Test aus Abbildung 3 wird fehlschlagen, wenn wir ihn ausführen – jedoch nicht weil unsere Methode Calc(int n1, int n2, operation operation) falsch implementiert ist, sondern weil der Fehler in der Klasse Addition vorliegt. Unser Unittests kann nicht bestanden werden, solange die Klasse Addition nicht korrekt implementiert ist.

 

Beispiel – Einsetzten eines Interfaces, um unabhängiges Testen zu ermöglichen

Um nun unsere Methode Calc(int n1, int n2, operation operation) dennoch unabhängig testen zu können, erstellen wir ein Interface IAddition (Abbildung 4). Dieses soll die Schnittstelle zwischen Calculator und Addition festlegen.


Abbildung 4 Interface um die Schnittstelle zur Klasse Addition zu definieren

Wie in Abbildung 5 gezeigt erbt die Klasse Addition von dem Interface IAddition. Aufgrund dessen ist die Klasse Addition verpflichtet eine Methode Add(int n1, int n2) mit einem Float Rückgabewert zu implementieren.


Abbildung 5 Unsere Klasse Addition erbt nun vom Interface IAddition

In der Klasse Calculator ändern wir nun den Typ der Variable Addition zu IAddition. Wir können sie immer noch mit einem Objekt vom Typ Addition initialisieren, da die Klasse Addition vom Interface IAddition erbt.


Abbildung 6 Den Typ der Variable Addition in unserem Calculator ändern

 

Was bedeutet dies nun für unseren Test? Wir können nun mithilfe vom NuGet Package FakeItEasy unser Interface „mocken“, sprich ein Objekt anlegen, welches diese Schnittstelle implementiert. In Abbildung 7 im Arrange-Block in Zeile 27 wird eine Mock-Instance fakeAddition erstellt, welche das Interface IAddition implementiert. Die Variable Addition von unserem Calculator wird dann in Zeile 28 mit fakeAddition ersetzt. Außerdem legen wir in Zeile 30 fest, dass, falls von unserem fakeAddition die Methode Add mit den Eingabeparametern number1 und number2 aufgerufen wird, der Wert (number1 + number2) zurückgegeben werden soll.


Abbildung 7 Anpassung des Unittests, um eine Mock-Objekt vom Typ IAddition zu erstellen und dieses anstelle der Klasse Addition zu nutzen

Dieser Unittest wird nun erfolgreich durchlaufen, wenn er ausgeführt wird, da dieser nicht mehr auf die Klasse Addition zugreift, sondern stattdessen auf unser Mock-Objekt fakeAddition. Unsere Klasse Calculator kann nun erfolgreich und unabhängig von der Klasse Addition getestet werden.

 

Beispiele – Was noch mit FakeItEasy gemockt werden kann

FakeItEasy bietet viele Möglichkeiten, um Tests ausführlicher und umfangreicher zu gestalten. Durch Interfaces und FakeItEasy können wir Mock-Instanz erstellen und festlegen, wie diese sich verhalten soll. Im Folgenden sind ein paar Beispiele aufgelisted, was für Mock-Instanzen festgelegt werden kann.

  • Wir können festlegen, wie oft eine Methode eines Interfaces aufgerufen werden darf, damit der Test bestanden werden kann
    A.CallTo(() => fakeAddition.Add(A.Ignored, A.Ignored)).MustHaveHappened(1, Times.Exactly);
  • Wir können Rückgabeparameter einer Methode für bestimmte Eingabeparameter festlegen
    A.CallTo(() => fakeAddition.Add(number1, number2)).Returns(number1 + number2
  • Wir können Rückgabeparameter einer Methode für beliebige Eingabeparameter festlegen
    A.CallTo(() => fakeAddition.Add(A.Ignored, A.Ignored)).Returns(3);
  • Wir können eine Abfolge von Rückgabe Parameter für eine Methode festlegen, welche nacheinander zurückgegeben werden sollen unabhängig davon was die Eingabeparameter sind
    A.CallTo(() => fakeAddition.Add(A.Ignored, A.Ignored)).ReturnsNextFromSequence(new float[] { 0.1f, 0.4f, 0.0f });
  • Wir können Exceptions festlegen, die geworfen werden sollen, wenn eine Methode unserer Mock-Instanz aufgerufen wird
    A.CallTo(() => fakeAddition.Add(A.Ignored, A.Ignored)).Throws();

Weiter Möglichkeiten FakeItEasy zu verwenden und Beispiele finden Sie unter: https://fakeiteasy.github.io/docs/7.3.1/

Fazit

Wir können Interfaces gezielt einsetzten, um Schnittstellen zwischen Klassen zu definieren. Diese Interfaces gewährleisten die unabhängige Testbarkeit dieser Klassen. Mittels FakeItEasy können die Interfaces genutzt werden, um den Informationsfluss über die Schnittstelle während Unittests zu kontrollieren. Dadurch können wir beispielsweise Rückgabeparameter von bestimmten Funktionsaufrufen festlegen, kontrollieren wie oft die Funktionen eines Interfaces aufgerufen werden, darf und vieles mehr.

Kontaktieren Sie uns!

Autor

Auch interessant:

Teil 2/2: Was ist ein Software-Entwicklungsplan?

Der Software Entwicklungsplan Teil: Was ist ein Software-Entwicklungsplan Teil: Wie sieht ein Software-Entwicklungsplan aus Wie sieht ein Software-Entwicklungsplan aus, gemäß den Anforderungen der 62304 Nachdem wir nun wissen, was ein Software-Entwicklungsplan ist, stellt sich natürlich die Frage, was der Inhalt eines solchen Planes konkret sein könnte. Eine gute Grundlage mit…
Getagged mit: , , , ,