An einem kleinen Beispiel möchte ich kurz erläutern, wie PEX parametrisierte Unit-Tests nutzt und wie man diese nutzen kann um bestimmte Test-Szenarien abzubilden. Wir nehmen einen kleinen Codeausschnitt um uns das mal anzusehen:
1: public class TotalSum
2: { 3: private double total = 0;
4:
5: public double CalculateTotals(List<cOrderPosition> OrderPositions, double Rebate)
6: { 7: if (OrderPositions == null)
8: return 0;
9:
10: foreach (cOrderPosition orderPos in OrderPositions)
11: { 12: if (orderPos.Amount > 0 && orderPos.SinglePrice > 0)
13: total += orderPos.Amount * orderPos.SinglePrice;
14: }
15:
16: if (Rebate > 0)
17: total = total * (1 - Rebate);
18:
19: return total;
20: }
21:
22: public class cOrderPosition
23: { 24: public int ProductID { get; set; } 25: public double Amount { get; set; } 26: public double SinglePrice { get; set; } 27: }
28: }
Auf den ersten Blick scheint da alles OK zu sein. Mal sehen was PEX daraus jetzt macht.
Zunächst sehen wir, dass PEX 3 Probleme mit Object Creations hat. Für den ersten Fall lassen wir PEX einfach automatisiert eine Factory erstellen indem wir auf "Accept/Edit factory" klicken. Für die Liste müssen wir ebenfalls eine Factory erstellen. Diese Factory wollen wir jetzt noch anpassen:
1: [PexFactoryMethod(typeof(List<TotalSum.cOrderPosition>))]
2: public static List<TotalSum.cOrderPosition> Create(int NumberOfItems)
3: { 4: List<TotalSum.cOrderPosition> list = new List<TotalSum.cOrderPosition>(NumberOfItems);
5: if (NumberOfItems > 10)
6: NumberOfItems = 10;
7:
8: for (int i = 0; i < NumberOfItems; i++)
9: { 10: list.Add(new TotalSum.cOrderPosition()
11: { 12: ProductID = i + 1,
13: SinglePrice = new Random().NextDouble() * 10,
14: Amount = new Random().NextDouble() * 10
15: });
16: }
17:
18: return list;
19: }
Abhängig von der Anzahl Items die als Parameter übergeben wird, wird die Liste mit entsprechend vielen Elementen befüllt. Nun erhalten wir Ergebnisse bei der Exploration.
Und es sind alle grün. Also alles OK? Jetzt kommt der parametrisierte Unit-Test in's Spiel. Dazu müssen wir erst mal die Test generieren. Dazu einfach alle Einträge markieren und rechts auf "Save..." klicken.
1: [TestMethod]
2: [PexGeneratedBy(typeof(TotalSumTest))]
3: public void CalculateTotals04()
4: { 5: TotalSum totalSum;
6: List<TotalSum.cOrderPosition> list;
7: double d;
8: totalSum = TotalSumFactory.Create();
9: list = ListFactory.Create(1);
10: d = this.CalculateTotals(totalSum, list, 0);
11: Assert.AreEqual<double>(42.232177096754121, d);
12: }
Wenn wir uns eine der generierten Testmethoden mal genauer anschauen, dann erkennen wir dass in Zeile 9 unsere ListFactory aufgerufen wird und in Zeile 10 wird eine Methode CalculateTotals aufgerufen. Bei dieser Methode handelt es sich um unseren parameterisierten Unit-Test der in der .cs-Datei abgelegt ist. Dieser parametrisierte Unit-Test nimmt Input-Werte engegen und ruft damit die eigentliche Funktion auf. Man kann den parametrisierten Unit-Test eigentlich mit einem datengetriebenen Test vergleichen, mit dem Unterschied dass die Daten nicht aus einer Datenquelle kommen sondern von den jeweiligen Testmethoden übergeben werden.
Wir können den parametrisierten Unit-Test selbst anpassen und auch Asserst einfügen. Was wir nun hier tun wollen, ist die eigentliche Test-Methode zwei mal aufzurufen und zu prüfen, ob beide Ergebnisse übereinstimmen. Bei gleichen Eingangswerten sollte dies ja der Fall sein. Der parametrisierte Unit-test sieht dann ungefähr so aus.
1: [TestClass]
2: [PexClass(typeof(TotalSum))]
3: [PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentException), AcceptExceptionSubtypes = true)]
4: [PexAllowedExceptionFromTypeUnderTest(typeof(InvalidOperationException))]
5: public partial class TotalSumTest
6: { 7: [PexMethod]
8: public double CalculateTotals(
9: [PexAssumeUnderTest]TotalSum target,
10: List<TotalSum.cOrderPosition> OrderPositions,
11: double Rebate
12: )
13: { 14: double result = target.CalculateTotals(OrderPositions, Rebate);
15: double result2 = target.CalculateTotals(OrderPositions, Rebate);
16: Assert.AreEqual(result, result2);
17: return result;
18: }
19: }
Diese Prüfung wird nun für alle Testmethoden ausgeführt. Und wie sieht das Ergebnis aus?
Wir erhalten nun einen Fehlerfall. Wenn man sich den Code der Test-Methode nochmals genauer anschaut, stellt man fest, dass die lokale Variable total beim erneuten Aufruf nicht zurückgesetzt wird - ein klassischer Fehler. Wenn wir die Variable in der Methode zurücksetzen, dann werden unsere Testfälle auch alle erfolgreich sein.
Somit haben wir mit Hilfe von PEX einen gängigen Fehler gefunden der in Real-World-Projekten sicher im Code selbst nicht so offensichtlich wäre.