Hallo Spaß Coder,
in den letzten Artikeln Testautomatisierung, Von Tests und Komponenten sowie Kein Unit-Test wie jeder andere haben wir euch heiß darauf gemacht, euren Code automatisiert auf funktionale Richtigkeit hin zu überprüfen. In diesem Artikel möchten wir euch ein paar Werkzeuge vorstellen, mit dessen Hilfe ihr euren .Net-Code automatisiert testen könnt. Wir setzen dabei wie immer C# als Sprache ein, die vorgestellten Werkzeuge funktionieren aber genauso gut mit VB.Net.
Für Werkzeuge, die ihr im Umfeld von Java verwenden könnt lest den Artikel Hör mal wer da testet – Java Edition.
Die hier verwendeten Beispiele testen eine Implementierung des Katas Russische Bauernmultiplikation aus dem Fundus der Coding Dojos der CCD.
NUnit
Mit Hilfe von NUnit können wir unseren Code einfach automatisch testen, ohne die Anwendung selbst starten zu müssen. Wir können Test-Code schreiben und darin die Methoden unserer Klasse aufrufen. NUnit führt diese Tests dann aus und bietet auch eine Möglichkeit, die Ergebnisse zu prüfen.
Das Visual Studio bindet NUnit mit Hilfe eines Plugins, die seit der Version 2013 auch in der kostenlosen Community Edition des VS verwendet werden können. Die Tests lassen sich dann mit einem Klick starten und zeigen die Ergebnisse des Testaufrufs komfortabel an. So zeigt ein grüner Balken, dass alle Tests erfolgreich durchlaufen wurden, ein roter, dass Tests fehlgeschlagen sind und ein gelber Balken zeigt an, dass Tests nicht ausgeführt wurden. Bei fehlgeschlagenen Tests sehen wir dann, wo der Test fehlschlägt und wir können uns sofort an die Behebung des Problems machen.
Beispiele:
Fangen wir mit einem einfachen Test an. Wir wollen testen, ob die Methode Mul 1×1 rechnen kann. Wir erwarten, dass dabei das Ergebnis 1 herauskommt.
1 2 3 4 5 6 |
[Test] public void OneTimesOneIsOne() { IMultiplikation sut = new RussischeBauernmultiplikation(); Assert.AreEqual(1, sut.Mul(1, 1)); } |
Das Attribut [Test] kennzeichnet hier die Testmethode, wodurch NUnit diese Methode als ausführbaren Test erkennt. Als nächstes erstellen wir eine Instanz unserer zu testenden Klasse RussischeBauernmultiplikation. Wir haben uns angewöhnt die zu testende Variable sut zu nennen, als Abkürzung für System Under Test oder Subject Under Test (siehe auch Von Tests und Komponenten). Dadurch erkennen wir auch bei komplexeren Testmethoden sofort, welche Klasse was wir eigentlich testen.
In der letzten Zeile wird nun geprüft, durch unsere Behauptung (englisch: Assertion), dass 1×1 = 1 ist. Der erste Parameter der Methode AreEqual ist dabei unser erwartetes Ergebnis (1) und der zweite Parameter das tatsächliche Ergebnis des Methodenaufrufs unserer Testklasse.
Funktioniert der Test, zeigt NUnit einen grünen Balken an. Schlägt der Test hingegen fehl, wird der Balken rot und NUnit zeigt uns an, welcher Test fehlgeschlagen ist. Hier eine beispielhafte Ausgabe von NUnit, wenn die Methode Mul(1, 1) statt der erwarteten 1 eine 2 zurück gibt:
de.invidit.RussischeBauernmultiplikation.Test.RussischeBauernmultiplikationTests.OneTimesOneIsOne:
Expected: 1
But was: 2
bei de.invidit.RussischeBauernmultiplikation.Test.RussischeBauernmultiplikationTests.OneTimesOneIsOne() in \RussischeBauernmultiplikation.Test\RussischeBauernmultiplikationNUnit\RussischeBauernmultiplikationTests.cs:Zeile 15.
Wir sehen also, dass der Aufruf in Zeile 15 der Test-Klasse RussischeBauernmultiplikationTests zu einem Fehler führt.
Auch der Aufbau des Tests, wie wir ihn im Artikel Von Tests und Komponenten beschrieben haben ist hier erkennbar. Hier nochmal eine deutlichere Aufteilung am Beispiel des Tests 2×2 = 4:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[Test] public void TwoTimesOneIsTwo() { // Initialisierung des Ausgangszustands RussischeBauernmultiplikation sut = new RussischeBauernmultiplikation(); int expected = 4; // Aufruf der zu testenden Methode int actual = sut.Mul(2, 2); // Prüfung des Ergebnisses Assert.AreEqual(expected, actual); } |
Neben der zu testenden Klasse legen wir auch unser erwartetes Ergebnis in einer Variablen ab. Passend zum Parameternamen der assert-Methoden von NUnit nennen wir die Variable expected. Dann rufen wir unsere zu testende Methode auf, also Mul und speichern das Ergebnis in der Variable actual. Zum Schluss prüfen wir die Behauptung (assertion), ob unsere Erwartung (expected) mit dem tatsächlichen (actual) Ergebnis übereinstimmt.
Auf diese Weise lassen sich die meisten Methoden einfach testen.
Microsoft Unit Testing Framework
Microsoft Unit Testing Framework funktioniert genauso wie NUnit und ist seit Version 2012 fester Bestandteil der kostenlosen Editionen des Visual Studios (VS Express und seit 2013 VS Community). Die Syntax unterscheidet sich von NUnit, die Einbindung in die IDE ist aber genauso gut (siehe oben bei der Beschreibung von NUnit).
Beispiele:
Für den Test mit dem Microsoft Unit Testing Framework werden eigene Attribute zur Kennzeichnung von Testklasse und -methoden verwendet.
1 2 3 4 5 6 |
[TestMethod] public void OneTimesOneIsOne() { IMultiplikation sut = new RussischeBauernmultiplikation(); Assert.AreEqual(1, sut.Mul(1, 1)); } |
So ersetzen wir einfach das Attribute [Test] von NUnit durch [TestMethod]. Dazu lege ich ein MS-Test Projekt an, womit auch die erforderlichen Verweise korrekt vorhanden sind. Über das Menü Test -> Run -> All Tests lassen sich dann alle Test der Klasse ausführen. Das Ergebnis wird im TestExplorer angezeigt.
Fluent Assertions
Die Bibliothek FluentAssertions erweitert das verwendete Testframework um einen Aspekt, der unsere Unit-Tests lesbarer macht. So liest sich die Überprüfung unserer Behauptung wie wir sie oben formuliert haben ein wenig holprig:
“Behaupte sind gleich, erwartet wie tatsächlich.”
Hä? Wer redet denn so einen Quatsch?
Wäre es nicht schöner, wenn wir unsere Behauptung in etwa so formulieren könnten:
“Tatsächliches Ergebnis sollte gleich sein mit erwartetem Ergebnis.”
Das Klingt zugegebenermaßen immer noch ein wenig holprig. Versuchen wir das Ganze mal in Englisch:
“Actual should be expected.”
In unseren Ohren klingt das doch ganz gut. FluentAssertions hilft uns dabei, unsere Behauptungen genau so zu formulieren.
Beispiele:
Schauen wir uns dasselbe Beispiel wie oben an, nämlich dass 1 x 1 = 1 ist (sein sollte).
1 2 3 4 5 6 |
[TestMethod] public void OneTimesOneIsOne() { IMultiplikation sut = new RussischeBauernmultiplikation(); sut.Mul(1, 1).Should().Be(1); } |
Hier ist die Lesbarkeit schon höher als oben beschrieben. Richtig spannend wird FluentAssertions in der Arbeit bestimmten Typen. Nachfolgend ein paar Ideen dazu:
- Strings
- BeNull / NotBeNull
- BeEmpty / NotBeEmpty
- HaveLength
- StartWith / Contain / EndWith
- Numerische Typen
- Be / NotBe
- BeGreaterOrEqualTo
- BeLessThan
- BeInRange
- Collections
- NoBeEmpty
- HaveCount
- StartWith / EndWith
- BeSubsetOf
- Contain / OnlyContain
- Datum
- BeAfter / BeBefore
- HaveDay / HaveMonth / HaveYear
- Ausnahmen
- ShouldThrow
- WithMessage
Darüber hinaus können Behauptungen auch kombiniert werden. Grundsätzlich sollte immer nur ein Konzept innerhalb eines Tests geprüft werden, aber FluentAssertions bietet auch die Möglichkeit Behauptungen zu verknüpfen (AND / OR).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[TestMethod] public void StringWithFluentAssertions() { string testString = "Dies ist ein Text"; testString.Should().Be("Dies ist ein Text"); testString.Should().NotBe("Dies ist kein Text"); testString.Should().BeEquivalentTo("DIES ist EIN Text"); testString.Should().Contain("t e"); testString.Should().NotContain("ist kein"); testString.Should().StartWith("Dies"); testString.Should().NotStartWith("Das"); testString.Should().StartWithEquivalent("dies"); testString.Should().EndWith("ein Text"); testString.Should().Be("Dies ist ein Text").And.Contain("t e"); } |
moq
Während wir mit den vorgenannten Werkzeuge grundsätzlich Unit-Tests schreiben können, hilft uns moq dabei unsere Tests von ggf. vorhandenen externen Einflüssen zu isolieren. Falls euch jetzt nicht klar sein sollte, was wir damit meinen, lest noch einmal den Artikel Von Tests und Komponenten. Hier wird auf die Notwendigkeit und die unterschiedlichen Möglichkeiten eingegangen.
Beispiele:
moq ist ein sehr mächtiges Werkzeug. In diesem Rahmen möchten wir euch die wichtigsten Anwendungsfälle zur Isolation erläutern, das Stubbing.
Wir rufen eine Methode auf und können mit Hilfe von moq festlegen, welchen Wert diese Methode zurückgeben soll. Der folgende Test wird erfolgreich durchlaufen:
1 2 3 4 5 6 7 8 |
[TestMethod] public void testMockitoSimpleWhenThenReturn() { var mock = new Mock<IMultiplikation>(); mock.Setup(imp => imp.Mul(1, 3)).Returns(5); mock.Object.Mul(1, 3).Should().Be(5); } |
Die Methode Mul(1, 3) unserer Implementierung gibt den Wert 3 zurück (1×3 = 3). Wir haben moq auf Basis des Interfaces aber folgendes gesagt:
“Wenn die Methode Mul mit den Parametern 1 und 3 aufgerufen wird, gebe den Wert 5 zurück.”
Damit wir das überhaupt tun können, müssen wir zunächst mit Hilfe von moq einen mock der Klasse erstellen, dessen Methodenrückgabe wir verändern möchten. In unserem obigen Fall handelt es sich dabei um ein Interface, es kann aber auch eine Klasse sein.
Wichtig zu wissen ist, dass bei einem Mock immer nur die Methoden einen Wert zurückgeben, die wir auch konfiguriert haben. Ergänzen wir noch folgenden Assert, wird der Test fehlschlagen:
1 |
mock.Object.Mul(1, 1).Should().Be(1); |
Test Name: testMockitoSimpleWhenThenReturn
Test FullName: TestExamples.MoqExamples.testMockitoSimpleWhenThenReturn
Test Outcome: Failed
Test Duration: 0:00:00,1369741Result Message: Expected 1, but found 0.
Die Methode Mul haben wir nur für die Parameter (1, 3) konfiguriert. Rufen wir sie mit (1, 1) auf, wird der Initialwert des Rückgabetyps (in unserem Fall int) zurückgegeben (dieser ist bei int 0).
Neben dem beschriebenen Stubbing wird beim Mocking nicht das eigentliche Testobjekt geprüft, sondern ob die Aufrufe auf dem Mock-Objekt wie erwartet erfolgt sind.
1 2 3 4 5 6 7 8 9 |
[TestMethod] public void testNextIsCalled() { var randMock = new Mock<Random>(); Dice sut = new Dice(2, randMock.Object); sut.Roll(); randMock.Verify(imp => imp.Next()); } |
Damit wird in unserem Beispiel die Methode Roll() indirekt geprüft. Dies kann dann sinnvoll sein, wenn geprüft wird, ob der Aufruf einer Abhängigkeit mit korrekten Parametern erfolgt.
Matcher
Möchten wir Methodenaufrufe mit beliebigen Parametern konfigurieren, müssen wir sog. Matcher verwenden:
1 2 3 4 5 6 7 8 9 |
[TestMethod] public void testMockitoSimpleWhenThenReturn() { var mock = new Mock<IMultiplikation>(); mock.Setup(imp => imp.Mul(It.IsAny<int>(), 3)).Returns(5); mock.Object.Mul(1, 3).Should().Be(5); mock.Object.Mul(2, 3).Should().Be(5); } |
Die Klasse Moq.It stellt verschiedene Matcher zur Verfügung, welche zur Konfiguration von Stubs verwendet werden können.
Ein “echtes” Beispiel
Die oben aufgeführten Beispiele sind natürlich nicht sinnvoll, sie dienen nur dem Zweck, die Funktionsweise von moq leicht verständlich zu beschreiben. Hilfreich ist ein Mock immer dann, wenn wir unsere Klasse von Abhängigkeiten trennen wollen.
Nehmen wir als Beispiel den Test der Klasse “Würfel”, dessen Methode “würfeln” einen zufälligen Wert verwendet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Dice { private int noOfDices; private Random random; public Dice(int noOfDices, Random random) { this.noOfDices = noOfDices; this.random = random; } public int Roll() { int sum = 0; for (int i = 0; i < noOfDices; i++) { sum += random.Next(); } return sum; } } |
Im Grunde ist die Methode nicht testbar, da hier immer andere Ergebnisse herauskommen, wenn von der Klasse Random die Methode Next() aufgerufen wird. Mit Hilfe von moq können wir die Methode aber trotzdem testen:
1 2 3 4 5 6 7 8 |
[TestMethod] public void testRollWithTwoDicesReturnsCorrectSum() { var randMock = new Mock<Random>(); randMock.Setup(imp => imp.Next()).Returns(4); Dice sut = new Dice(2, randMock.Object); sut.Roll().Should().Be(8); } |
Dieser Test funktioniert. In dem wir festlegen, dass beim Aufruf von Next() immer ein fester Wert zurückkommt, können wir testen, ob die Methode Roll() die Summe der Einzelwürfe korrekt addiert.
Unsere Erfahrung mit den Werkzeugen
Wir setzen insbesondere Microsoft Unit Testing Framework und Fluent Assertions ein, wenn wir unsere .Net-Projekte entwickeln. Früher haben wir NUnit verwendet, da Microsoft Unit Testing Framework in den Express-Editionen des Visual Studios nicht zur Verfügung stand. Da Fluent Assertion beide Frameworks unterstützt und Microsoft Unit Testing Framework mittlerweile immer „an Bord“ ist, verwenden wir dies aus reiner Bequemlichkeit. Sie leisten uns gute Dienste und unterstützen dabei, den Code abzusichern. Mit moq haben wir erst sehr wenig Erfahrung gesammelt und faktisch bislang in keinem Projekt eingesetzt. Wir haben unsere Mocks bislang immer manuell erstellt. Das werden wir aber in Zukunft sicher ändern, da es ein großartiges Werkzeug ist.
Zusammenfassung
Mit Hilfe der hier vorgestellten Werkzeuge und Beispiele solltet ihr in der Lage sein, euren Code elegant und automatisiert testen zu können. NUnit und Microsoft Unit Testing Framework bilden die Basis aller Unit-Tests. Fluent Assertions erweitert – die bereits sehr elegante Schreibweise von NUnit seit Version 2.4 (Constraint-Based Assert Model) – und Microsoft Unit Testing Framework um nützliche Funktionen und sinnvolle, einfach lesbare Syntax. moq hilft dabei, Tests von der umliegenden Infrastruktur zu trennen und auch Legacy-Code testbar zu machen ohne gleich die gesamte Code-Basis auf den Kopf stellen zu müssen.
Wir hoffen, dass es euch hiernach leichter fällt, euren sauberen Code auch funktional abzusichern.
Viel Spaß beim Anwenden.
Eure Spaß-Coder
Weitere Infos zu den Bibliotheken:
- NUnit – http://www.nunit.org/
- NUnit Plugin für Visual Studio – http://www.nunit.org/index.php?p=vsTestAdapter&r=2.6.2
- Microsoft Unit Testing Framework – https://msdn.microsoft.com/en-us/library/ms243147.aspx
- FluentAssertions – http://www.fluentassertions.com/
- moq – https://github.com/Moq/moq4#moq