Kürzlich habe ich meinen Kollegen aus der PHP Entwicklung gezeigt, wie man mit statischem Code aus dem Framework umgeht, zu dem es keine Interfaces gibt. Heute kam auf der User Group Karlsruhe nochmals das Thema in einem Dialog auf sodass ich das Beispiel hier publizieren will.
Dazu dient mir die Console-Klasse aus dem .NET Framework. Ich könnte jetzt auf die häufig verwendete WriteLine-Methode eingehen, allerdings will ich die Aufgabe ein wenig erschweren, indem ich die ReadKey-Methode mocken bzw. faken will. Damit haben wir auch gleich das Thema User Interaktion abgedeckt.
Zunächst noch eine Klarstellung: Von Fakes spricht man, wenn man Dummy-Implementierungen selbst schreibt. Just in diesem Moment habe ich Dr. House geschaut und da kam der Ausspruch “das war bloß gefakt” vor. Eine gute Eselbrücke, wie ich finde, da man für einen Fake eben aktiv werden muss. Mocking bezeichnet das automatische Erstellen von Fakes anhand von Interfaces dynamisch zur Laufzeit. Die dazu benötigten Proxies werden können mit Mocking Frameworks erzeugt werden. Beide Verfahren werde ich zeigen.
Schauen wir uns den eigentlichen Code an, den wir dann verbessern, um ihn zu testen:
1: public void Process()
2: {
3: var userChoice = Console.ReadKey();
4: switch (userChoice.Key)
5: {
6: case ConsoleKey.S:
7: //Logik A ausführen
8: break;
9:
10: default:
11: //Logik B ausführen
12: break;
13: }
14: }
Wie können wir das testen? Ideal wäre, wenn es ein Interface für die Console Klasse gäbe. Gibt es aber nicht. Also erstellen wir unser eigenes:
1: public interface IConsoleHelper
2: {
3: ConsoleKeyInfo ReadKey();
4: }
Die Implementierung für unser eigentliches Programm ist das nichts anderes als ein Wrapper für die ursprüngliche Logik:
1: public class ConsoleHelper : IConsoleHelper
2: {
3: public ConsoleKeyInfo ReadKey()
4: {
5: return Console.ReadKey();
6: }
7: }
So weit so gut. Nun muss das ursprüngliche Programm diese Abhängigkeit reingereicht bekommen. Wie, das ist euch überlassen. Entweder ihr übergebt die Implementierung per Constructor Injection oder per Method Injection. Ich halte es immer so, dass alle für den eigentlich Logikablauf essentielle Abhängigkeiten über den Konstruktor reingereicht und alle optionalen Abhängigkeiten (wie einen Logger) über eine Methode (z.B. SetLogger(ILogger logger)) injiziert werden. Egal wie ihr das implementiert, innerhalb des Programms gibt es dann die Instanzvariable _consoleHelper vom Typ IConsoleHelper. Der Code sieht dann wie folgt aus:
1: public void Process()
2: {
3: var userChoice = _consoleHelper.ReadKey(); //<- Hier liegt der Unterschied
4: switch (userChoice.Key)
5: {
6: case ConsoleKey.S:
7: //Logik A ausführen
8: break;
9:
10: default:
11: //Logik B ausführen
12: break;
13: }
14: }
Nun zum Testen. Euer Unit Test müsste nun prüfen, o bei der Eingabe von S Logik A und bei sonstigen Eingaben Logik B ausgeführt wird.
Zuerst die Möglichkeit mit einem selbstgeschriebenen Fake, den ich zum Testen des ersten Falls, nämlich der Eingabe von S, nehme:
1: /// <summary>
2: /// Eigene Fake-Implementierung zum faken von Konsoleneingaben
3: /// </summary>
4: public class ConsoleFake : IConsoleHelper
5: {
6: public ConsoleKeyInfo ReadKey()
7: {
8: return new ConsoleKeyInfo('s', ConsoleKey.S,false,false,false);
9: }
10: }
Und hier die Implementierung eines Mocks mit dem Framework Fake It Easy:
1: var consoleFake = A.Fake<IConsoleHelper>();
2: A.CallTo(() => consoleFake.ReadKey())
3: .Returns(new ConsoleKeyInfo(
4: 'a', ConsoleKey.A, false, false, false)
5: );
Schaut euch auch den Artikel über das Mocken von DateTime.Now an.