Hallo Spaß-Coder.
Unit Tests sind eine coole Sache. Warum? Weil ich vor meinen Änderungen am Code leicht prüfen kann, ob alles wie beabsichtigt funktioniert. Dann mache ich meine Änderungen und kann anschließend wieder prüfen, ob immer noch alles funktioniert. Damit sinkt für mich die Gefahr, bei Codeänderungen bestehende Funktion kaputt zu machen.
Es muss nicht zwingend testgetrieben entwickelt werden. Selbst später Unit Tests hinzuzufügen kann hilfreich sein
- um den Code zu verstehen und
- um meine Änderung abzusichern (inhaltliche oder strukturelle Änderung).
In verschiedenen Programmiersprachen werden mittlerweile Unit Test Framework bereitgestellt, welche das Schreiben und Ausführen von Unit Test unterstützen. Für .Net sind Microsoft UnitTesting und NUnit bekannte Frameworks. Auf letzteres werden wir hier nicht eingehen, mehr Infos dazu sind unter staratnight.de/blog zu finden.
Machen wir ein kleines Beispiel zu UnitTests mit Microsoft UnitTesting und nutzen dazu diese einfache Klasse mit einer Methode zur Berechnung der ersten n Fibonacci Zahlen nach fn = fn-1 + fn-2 für n > 2 mit f1 = f2 = 1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.Collections.Generic; namespace Fibonacci { public class Fibonacci { public List<int> GetFirstNNumbers(int n) { List<int> fibonacciNumbers = new List<int>(); int firstNumber = 1; int secondNumber = 1; fibonacciNumbers.Add(firstNumber); fibonacciNumbers.Add(secondNumber); for (int i = 2; i < n; i++) { fibonacciNumbers.Add(fibonacciNumbers[i - 2] + fibonacciNumbers[i - 2]); } return fibonacciNumbers; } } } |
Wenn wir diesen Code vorfinden und gerne ändern möchten, ohne dabei die Funktion kaputt zu machen, können wir zunächst Unit Test dafür schreiben. Dafür füge ich der Projekt Solution ein neues Unit Test Projekt hinzu und ergänze dort den Verweis auf das zu testende Projekt Fibonacci. Dies sind dann wie folgt aus:
Für die Namensgebung des Projekts wird einfach ein „.Test“ an den Projektnamen des zu testenden Projekts angehängt. Visual Studio erzeugt eine Testklasse mit der Grundstruktur eines Unit Tests als Vorlage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Fibonacci.Test { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { } } } |
Zunächst wird der Klassenname an die zu testende Klasse angepasst: aus UnitTest1 wird damit FibonacciTests. Wichtig sind die beiden Attribute [TestClass] und [TestMethod]. Daran erkennt das Testframework, was bei der Ausführung der Tests zu berücksichtigen ist.
Was wollen wir zuerst prüfen? Beginnen wir mit einfachen Fällen und testen die ersten 2 Zahlen auf Richtigkeit. Für jeden Test schreiben wir eine Testmethode mit dem folgenden Aufbau:
- Vorbereiten
- Durchführen
- Prüfen
Ich beginne jetzt bewusst mit dem Test für n = 2, weil ich weiß, dass n = 1 nicht funktioniert, was wir spätzer ergänzen können.
1 2 3 4 5 6 7 8 9 10 11 |
[TestMethod] public void First2NumbersAreCorrect() { Fibonacci sut = new Fibonacci(); var numbers = sut.GetFirstNNumbers(2); Assert.AreEqual(1, numbers[0]); Assert.AreEqual(1, numbers[1]); } } |
Der Methodenname sollte sprechend gewählt werden, damit die Absicht des Tests direkt klar wird.
Die Abkürzung sut steht hierbei für subject under test. So kann ich bei vielen Variablen innerhalb des Tests leicht erkennen, was eigentlich zu testen ist. Dies ist die Vorbereitung, eine Instanz der zu testenden Klasse erstellen.
Anschließend wird die zu testende Methode aufgerufen und abschließend das Ergebnis geprüft.
Die Ergebnisprüfung geschieht in Unit Test mit assertions, also mit Behauptungen. Hier behaupten wir, dass die Fibonacci Zahl am Index 0 = 1 ist, ebenso wie die Zahl am Index 1. Die Assert-Klasse von MS UnitTesting stellt einge Behauptungen bereit, z.B. um Boolsche-Ausdrücke oder auf Null zu prüfen.
Lasse ich nun über das Menü von Visual Studio „Test / Run / All Tests“ alle Test laufen, so wird mir das Ergebnis des Tests im Test Explorer angezeigt.
Ab jetzt kann ich nach jeder kleinen Änderung mit der Tastenkombination Strg+R, A alle Tests laufen lassen und erhalte somit ein Feedback, ob meine Änderung die Funktion der bereits geprüften Methoden verändert hat. Grün heißt, alles ok. Bei einem fehlerhaften Test wird dies in rot angezeigt, so dass ich zunächst den Fehler korrigieren kann, bevor ich weitermache.
Erweitern wir die Testmethoden um die Prüfung auf die ersten vier Fibonacci Zahlen.
1 2 3 4 5 6 7 8 9 10 |
[TestMethod] public void First4NumbersAreCorrect() { Fibonacci sut = new Fibonacci(); var numbers = sut.GetFirstNNumbers(4); Assert.AreEqual(1, numbers[0]); Assert.AreEqual(1, numbers[1]); Assert.AreEqual(2, numbers[2]); Assert.AreEqual(3, numbers[3]); } |
Das Testergebnis zeigt und einen Fehler – wird ja Zeit, dass hier mal jemand testet!
Als vierte Fibonacci Zahl erwarten wir die 3, tatsächlich gibt die Methode aber 2 zurück. Nun lässt sich gezielt dieser Test debuggen und siehe da – die Methode hat noch nie funktioniert, da sich bei der Berechnung ein Fehler eingeschlichen hat – oh man! Wir korrigieren die Zeile wie folgt und addieren den Vorgänger mit dem Vorvorgänger.
1 2 3 4 |
for (int i = 2; i < n; i++) { fibonacciNumbers.Add(fibonacciNumbers[i - 1] + fibonacciNumbers[i - 2]); } |
Ein erneuter Aufruf der Tests bestätigt – so ist’s besser.
Nun könnte ich weitere Unit Tests ergänzen und anschließend den Code der Fibonacci-Klasse umbauen oder ergänzen und kann durch immer wieder ausgeführte Tests prüfen, ob noch alle Ergebnisse wie erwartet berechnet werden.
An dieser Stelle verzichte ich allerdings darauf und zeige noch eine andere Möglichkeit mit Behauptungen – assertions – zu arbeiten. Dazu installiere ich per NuGet die Bibliothek FluentAssertions für mein Testprojekt.
Diese Bibliothek ist als fluent interface aufgebaut, was bedeutet, dass die Methodenaufrufe sehr sprechend formuliert werden. Dabei wird immer von dem zu überprüfenden Objekt ausgegangen, welches ein bestimmtes Verhalten aufweisen sollte. Unser zweiter Unit Test könnte damit so aussehen:
1 2 3 4 5 6 7 |
[TestMethod] public void First4NumbersAreCorrect() { Fibonacci sut = new Fibonacci(); var numbers = sut.GetFirstNNumbers(4); numbers.Should().BeEquivalentTo(new int[] { 1, 1 , 2, 3}); } |
Dieses Beispiel hat leider nicht das Potential die Möglichkeiten und die Lesbarkeit von FluentAssertions in voller Breite zu demonstrieren. Dennoch kann die Behauptung nun als echter Satz gelesen und damit sehr leicht verstanden werden. In unserem Fall:
numbers.Should().BeEquivalentTo(new int[] { 1, 1 , 2, 3});
Also: numbers sollte equivalent sein zu einer Liste mit den Werten 1, 1, 2, 3.
Die Grammatik funktioniert im englischen allerdings besser, weshalb der Code nochmal besser lesbar ist, als diese „Übersetzung“.
Diese Art der Assertions sind selbst dann gut lesbar, wenn ich die Signatur der Assert-Methoden – also welche Parameter was bedeuten – nicht kenne.
Wichtig: das Testergebnis zeigt weiterhin an, dass alles wie erwartet funktioniert 🙂
Zusammenfassung
In diesem Artikel haben wir uns grundsätzliche Unit Tests angeschaut und haben hinterfragt, warum diese hilfreich sind. Wir haben das Testframework Microsoft UnitTesting genutzt sowie die Erweiterung für sprechende Behauptungen mit FluentAssertions.
Viel Spaß beim Ausprobieren und Happy Coding wünschen
eure Spaß-Coder
Verwendete Werkzeuge:
- Visual Studio 2013 Community (https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx)
- Microsoft UnitTesting (https://msdn.microsoft.com/de-de/library/hh694602.aspx)
- Fluent Assertions (https://github.com/dennisdoomen/fluentassertions)