Microsoft Research hat vor kurzer Zeit PEX zum freien Download veröffentlicht. Hinter diesem unscheinbaren Kürzel verbirgt sich ein Tool das absolut genial und beeindruckend ist und klar mach, warum Microsoft ein research center unterhält. Das Tool verspricht nichts weniger als die automatische Generierung von Unit-Tests und den dazugehörigen Testcases um eine möglichst hohe Code-Abdeckung zu erzielen. Detaillierte Informationen bietet das Whitepaper, wer sich auf die schnelle einen Einblick verschaffen möchte findet einen Überblick im Folgenden (na ja, für einen Überblick ist der Post vielleicht doch ein wenig lang geraden, aber ich konnte mich nicht bremsen vor Begeisterung):
Darf ich vorstellen - PEX
Gegeben sei folgende Methode die getestet werden soll:
1: public string SimpleTest(int x1, int x2)
2: { 3: if (x1 > x2)
4: return "x1 > x2";
5: if (x1 < x2)
6: return "x1 < x2";
7: else
8: return "x1 == x2";
9: }
Für das versierte Auge eines Entwicklers ist sofort klar, da brauchen wir 3 Testcases um eine vollständige Code-Abdeckung zu erzielen. Mal sehen, was PEX daraus macht. Zunächst mal muss PEX heruntergeladen und installiert sein. Dann kann man einfach einen "Parameterized Unit Test Stub" erzeugen. Dazu in der Methode rechts klicken und den Befehl aus dem Pex-Menü auswählen.
Im folgenden Dialog können Sie verschiedene Parameter angeben. Das wichtigste hier ist das Testprojekt in dem der Stub erzeugt werden soll.
Der erzeugte Stub sieht dann so aus:
1: /// <summary>
2: /// This class contains parameterized unit tests for Calculation
3: /// </summary>
4: [TestClass]
5: [PexClass(typeof(Calculation))]
6: public partial class CalculationTest
7: { 8: [PexMethod]
9: public string SimpleTest(
10: [PexAssumeUnderTest]Calculation target,
11: int x1,
12: int x2
13: )
14: { 15: string result = target.SimpleTest(x1, x2);
16: return result;
17: // TODO: add assertions to method CalculationTest.SimpleTest(Calculation, Int32, Int32)
18: }
19:
20: }
Bei diesem Stub handelt es such um eine Vorlage für einen parameterisierten Unit-test. Toll, und was lässt sich damit nun machen? Wir können eine "Exploration" starten.
Diese Exploration versucht nun Input-Parameter zu finden die zu einer möglichst 100%igen Code-Abdeckung führen. Und hier beginnt nun die Magic von PEX. Ohne unser Zutun findet PEX 3 Kombinationen von Input-Parametern die tatsächlich eine komplette Code-Abdeckung erzielen- WOW! Dazu analysiert PEX wirklich den von uns erstellten Code und kann daraus definieren, mit welchen Input-Parametern die einzelnen noch nicht abgedeckten Zweige erreicht werden können.
Und das schönste, PEX baut uns automatisch 3 Unit-Tests die diese Testcases implementieren:
1: [TestMethod]
2: [PexGeneratedBy(typeof(CalculationTest))]
3: public void SimpleTest01()
4: { 5: string s;
6: Calculation calculation = new Calculation();
7: s = this.SimpleTest(calculation, 1, 2);
8: Assert.AreEqual<string>("x1 < x2", s); 9: }
10:
11: [TestMethod]
12: [PexGeneratedBy(typeof(CalculationTest))]
13: public void SimpleTest02()
14: { 15: string s;
16: Calculation calculation = new Calculation();
17: s = this.SimpleTest(calculation, 1879212556, 1879212556);
18: Assert.AreEqual<string>("x1 == x2", s); 19: }
20:
21: [TestMethod]
22: [PexGeneratedBy(typeof(CalculationTest))]
23: public void SimpleTest03()
24: { 25: string s;
26: Calculation calculation = new Calculation();
27: s = this.SimpleTest(calculation, 256, 254);
28: Assert.AreEqual<string>("x1 > x2", s); 29: }
Diese Unit-Tests können wir nun starten und sehen, dass diese wie erwartet alle erfolgreich sind.
Damit haben wir einen Test automatisiert erstellt, der sicherstellen kann, dass diese Methode ihr Verhalten nach Außen für die aktuell definierten Test-Cases nicht verändert. Damit können ungewollte Änderungen an der Methode erkannt und beseitigt werden. Ob die Methode allerdings ihre Aufgabe korrekt erledigt, kann PEX natürlich nicht testen. Haben wir die Funktionsweise einer Methode allerdings einmal validiert, kann PEX nun sehr einfach dieses Verhalten prüfen. Und natürlich eignet es sich auch sehr gut um mögliche Test-Cases zu definieren. Es müssen in diesem Fall dann nur noch die einzelnen Ergebnisse je Test validiert werden.
Ändern wir die Methode ab, so dass sich ihr Verhalten ändert, dann alarmiert uns der entsprechende Test.
Soweit sogut - Und was geht sonst noch?
Wenn wir nun ein gewünschte Änderung der Funktionalität implementieren, wie kann PEX dann damit umgehen? Zunächst würden wir die vorhandenen Tests durchführen, damit wir sicher sind, dass die aktuelle Funktionalität noch korrekt läuft. Dann erweitern wir unsere Methode:
1: public string SimpleTest(int x1, int x2)
2: { 3: if (x1 > x2 * 2)
4: return "x1 > x2 * 2";
5: if (x1 > x2)
6: return "x1 > x2";
7: if (x1 < x2)
8: return "x1 < x2";
9: else
10: return "x1 == x2";
11: }
Zeile 3+4 haben wir neu hinzugefügt. Nun starten wir eine neue Exploration und PEX ermittelt einen weiteren Test-Case um diese Funktion ebenfalls abzudecken.
Schön - darf's noch ein bisschen mehr sein?
Dieses einfache Sample war ja schon sehr beeindruckend. Die Frage, die sich aber natürlich direkt stellt, ist wie weit geht denn das? Wir wollen nun den Schwierigkeitsgrad für PEX schrittweise steigern. Integer-Werte sind ja noch relativ einfach zu handhaben, aber wie sieht's denn beispielsweise mit Strings aus? Hierzu zunächst wieder eine Methode, die wir testen wollen:
1: public class StringOperations
2: { 3: public string CheckString(string Input)