Blog Home  Home Feed your aggregator (RSS 2.0)  
artiso Blog - Testing
Neues rund um's Thema .Net
 
 Thursday, April 02, 2009

Der Tatsache, dass Testen eine wichtige Bedeutung in der Software-Entwicklung hat, trägt Microsoft ja bereits seit einiger Zeit Rechnung indem eine spezielle Edition des Visual Studio für Tester generiert wurde. Diese Edition besitzt umfangreiche Funktionenzu verschiedenen Test-Methoden. Das Ranger-Team hat nun einen Quick Reference Guide veröffentlicht der auf 83 Seiten diese Funktionen beschreibt und verschiedene Best Practices anbietet. Hierbei sind die Erfahrungen eingeflossen, die Service Labs, ein großes Test-Center, bei der Adaption von Team Test gemacht hat. Das Dokument enthält viele wertvolle Hinweise, wie VSTT in der Praxis eingesetzt und individuell erweitert werden kann.

Definitiv lesenswert für jeden, der etwas mehr über dieses Toolset erfahren möchte.

Download VSTT 2008 Quick Reference Guide

Thursday, April 02, 2009 7:33:23 AM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |   | 
 Wednesday, April 01, 2009

Das Thema Test Driven Development oder auch Test First Developent gewinnt immer mehr an Beachtung. Keine Konferenz, keine Zeitschrift, kein Sprecher der was auf sich hält kommt um das Thema herum. Doch nach dem überzeugenden Vortrag sitzt man zu Hause im Büro vor einem leeren Project und wie nun anfangen? Hier scheitern bereits die ersten, weil entsprechende Publikationen oft zwar die Vorteile ausführlich schildern, aber nicht den Einstieg darstellen. Deshalb möchte ich hier einen entsprechenden Einstieg geben und mit einem wirklich leeren Projekt beginnen.

Die Theorie um TDD will ich hier einfach weglassen. Hierzu gibt es bereits Informationen genug. Und wir werden verschiedene Vereinfachung vornehmen, über die Profis etwas die Nase rümpfen werden, aber damit erhalten wir ein einfaches und praktikable Einstiegsszenario.

Zum Einsatz kommen hierbei die Testfunktionen von Visual Studio 2008 die ab der Professional Edition enthalten sind. Wir beginnen mit einer komplett leeren Solution.

image

Die Frage, die nun im Raum steht, ist: Wie schreibe ich einen Test ohne eine Methode zu haben. Ein Unit-Test besteht ja im Prinzip darin, dass wir eine Methode aufrufen und den Rückgabewert mit einem Erwartungswert vergleichen. Der Test wird aber nicht einmal kompilieren, solange die Methode nicht definiert ist. Der Workaround an dieser Stelle sieht dann oft so aus, dass man von der Methode und ihrer Klasse erst einmal einen Stub anlegt der im wesentlichen eine “ThrowNotImplemented”-Exception wirft. Damit haben wir aber eigentlich schon mehr implementiert als nach dem TDD uns lieb ist.

Ein etwas eleganterer Ansatz geht über die Definition von Interfaces. Diese Vorgehensweise eignet sich besonders gut bei einer komponentenorientierten Architektur mit einem Contract First Ansatz. Dabei werden die Schnittstellen der einzelnen Komponenten erst über Contracts (Interfaces) beschrieben bevor diese implementiert werden. Den TDD-Ablauf Rot > Grün > Refactor erweitern wir ein wenig. Damit ergibt sich folgende Abfolge:

Contract definieren > Test implementieren > Rot > Funktion implementieren > Grün > Refactor

D.h. wir erstellen in einem ersten Schritt einen Contract (genau genommen machen wir damit kein TDD sondern ein Test First. Beim TDD ist der Test das erste was erstellt werden muss, aber das ist in meinen Augen eher Haarspalterei, so funktioniert es einfach in der Praxis). Wir erstellen ein neues ClassLibrary-Projekt und erstellen dort ein Interface.

   1: namespace Contracts
   2: {
   3:     public interface IOrderCalculator
   4:     {
   5:         decimal CalculateShippinghCosts(decimal sum, decimal freeShippingMin, decimal shippingCosts);
   6:     }
   7: }

Wir wollen hier ein überschaubares, aber auch nicht zu triviales Beispiel verwenden. Die Methode CalculateShippingCosts soll zu einem gegebenen Rechnungsbetrag Versandkosten hinzuaddieren, wenn ein bestimmter Mindestbetrag nicht erreicht ist. So damit haben wir den Contract erstellt. Nun wollen wir einen Test dazu erstellen. Das geht am schnellsten durch einen Rechts-Klick auf die Methode und dann “Create Unit-Tests”.

image

Hier wird standardmäßig ein neues Test-Projekt angelegt. Darin wird ein entsprechender Unit-Test generiert.

   1: [TestMethod()]
   2: public void CalculateShippinghCostsTest()
   3: {
   4:     IOrderCalculator target = CreateIOrderCalculator(); // TODO: Initialize to an appropriate value
   5:     Decimal sum = new Decimal(); // TODO: Initialize to an appropriate value
   6:     Decimal freeShippingMin = new Decimal(); // TODO: Initialize to an appropriate value
   7:     Decimal shippingCosts = new Decimal(); // TODO: Initialize to an appropriate value
   8:     Decimal expected = new Decimal(); // TODO: Initialize to an appropriate value
   9:     Decimal actual;
  10:     actual = target.CalculateShippinghCosts(sum, freeShippingMin, shippingCosts);
  11:     Assert.AreEqual(expected, actual);
  12:     Assert.Inconclusive("Verify the correctness of this test method.");
  13: }

Damit können wir den Test bereits zum ersten mal ausführen und er geht wie erwartet auf Rot. Aber Moment, wie funktioniert das. Wie kann der Test eine Methode auf einem Interface aufrufen? Es gibt ja noch keine Implementierung des Interfaces und ein Interface selbst lässt sich ja nicht instanziieren. Hier baut Visual Studio einen kleinen Workaround. In Zeile 4 im obigen Code sieht man, dass eine Instanz des target-Objektes über die Methode CreateIOrderCalculator() erstellt wird. Diese Methode wollen wir mal etwas genauer anschauen.

   1: internal virtual IOrderCalculator CreateIOrderCalculator()
   2: {
   3:     // TODO: Instantiate an appropriate concrete class.
   4:     IOrderCalculator target = null;
   5:     return target;
   6: }

Hier wird das Objekt einfach mit null initialisiert. Ein einfacher, aber wirkungsvoller Workaround. Damit erreichen wir unser Ziel, dass der Test kompiliert aber fehlschlägt. Nach der Implementierung ersetzen wir das null einfach durch die entsprechende Initialisierung. Damit können wir nun unsere Testcases Implementieren.

   1: [TestMethod()]
   2: public void CalculateShippinghCosts_Sum_Below_FreeShippingMin()
   3: {
   4:     IOrderCalculator target = CreateIOrderCalculator();
   5:     Decimal sum = 1;
   6:     Decimal freeShippingMin = 10;
   7:     Decimal shippingCosts = 5;
   8:     // We are below min, so we have to add shippingCosts
   9:     Decimal expected = 6;
  10:     Decimal actual;
  11:     actual = target.CalculateShippinghCosts(sum, freeShippingMin, shippingCosts);
  12:     Assert.AreEqual(expected, actual);
  13: }
  14:  
  15: [TestMethod()]
  16: public void CalculateShippinghCosts_Sum_Above_FreeShippingMin()
  17: {
  18:     IOrderCalculator target = CreateIOrderCalculator();
  19:     Decimal sum = 20;
  20:     Decimal freeShippingMin = 10;
  21:     Decimal shippingCosts = 5;
  22:     // We are above min, so we don't add shippingCosts
  23:     Decimal expected = 20;
  24:     Decimal actual;
  25:     actual = target.CalculateShippinghCosts(sum, freeShippingMin, shippingCosts);
  26:     Assert.AreEqual(expected, actual);
  27: }
  28:  
  29: [TestMethod()]
  30: public void CalculateShippinghCosts_Sum_Equal_FreeShippingMin()
  31: {
  32:     IOrderCalculator target = CreateIOrderCalculator();
  33:     Decimal sum = 10;
  34:     Decimal freeShippingMin = 10;
  35:     Decimal shippingCosts = 5;
  36:     // We are equal min, so we don't add shippingCosts
  37:     Decimal expected = 10;
  38:     Decimal actual;
  39:     actual = target.CalculateShippinghCosts(sum, freeShippingMin, shippingCosts);
  40:     Assert.AreEqual(expected, actual);
  41: }

Damit haben wir unser Szenario ausreichend beschrieben. Wir können nun an die Implementierung gehen.Dazu legen wir ein neues Projekt an in dem wir eine Klasse definieren die wir von unserem Interface ableiten.

   1: namespace Components
   2: {
   3:     public class cOrderCalculator : IOrderCalculator
   4:     {
   5:         public decimal CalculateShippinghCosts(decimal sum, decimal freeShippingMin, decimal shippingCosts)
   6:         {
   7:             // If sum is greater than Min then don't add shipping costs
   8:             if (sum > freeShippingMin)
   9:                 return sum;
  10:             else
  11:                 // else add shipping costs
  12:                 return sum + shippingCosts;
  13:         }
  14:     }
  15: }

Nun müssen wir unbedingt noch daran denken, die Initialisierung des Testobjektes in unserer Testmethode anzupassen.

   1: internal virtual IOrderCalculator CreateIOrderCalculator()
   2: {
   3:     IOrderCalculator target = new cOrderCalculator();
   4:     return target;
   5: }

Nun können wir die Tests ausführen.

image

Oh, ein Test schlägt fehl. Bei genauerer Betrachtung stellen wir fest, dass wir bei der Implementierung den Fall dass die Summe gleich der Grenze ist nicht richtig berücksichtigt haben. Also hat sich hier der TDD-Ansatz schon bewährt und wir können den Fehler beheben. Damit sind alle Tests grün und wir können weiter fortfahren. Wir könnten nun z.B. auf unserem Interface weitere Methoden definieren und dafür Tests anlegen.

Also eigentlich gar nicht so schwer das mit dem TDD, oder? Freue mich auf euer Feedback.

Die Solution gibt es zum Download.

Happy Testing!

Wednesday, April 01, 2009 1:10:53 AM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [2]    |  |  |   | 
 Tuesday, February 24, 2009

MSDN Webcasts 

Zusammen mit Christian Binder habe ich einen WebCast aufgenommen in dem wir PEX vorstellen. Der WebCast wird ab 12.03 online sein.

Details zur Veranstaltung: Unit-Test Generierung mit PEX [1032405246] - Microsoft Deutschland GmbH

Tuesday, February 24, 2009 9:06:26 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |   | 
 Friday, February 20, 2009

MSDN Webcasts

Zusammen mit Christian habe ich einen Webcast zum Thema Testing Practices aufgenommen. In dem Webcast werden zunächst verschiedene Testmethoden vorestellt und anschließend werden verschiedene Aspekte einer Teststrategie diskutiert wie z.B. Methoden zur Testfall-Ermittlung.

Der komplette Abstract lautet:

Qualität spielt in Software-Projekten eine immer größere Rolle. Ein wesentlicher Aspekt für Software-Qualität ist das Testen. Der Webcast stellt zunächst kurz die verfügbaren Testmethoden in Visual Studio vor und zeigt anschließend Aspekte einer Test-Strategie auf. Darüber hinaus wird die Integration mit dem Team Foundation Server kurz beleuchtet und es werden Methoden zur Testfallermittlung beschrieben.

Über ein Feedback zum Webcast würde ich mich freuen.

http://www.microsoft.com/germany/msdn/webcasts/library.aspx?id=1032405240

Friday, February 20, 2009 7:12:30 AM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |  |  |   | 

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.

image

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.

image

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.

image 

   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?

image

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.

Friday, February 20, 2009 1:20:23 AM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [1]    |  |   | 
 Thursday, February 19, 2009

Ich habe in einem früheren Post bereits einige Beispiele für den Einsatz von PEX beschrieben. ich möchte nun in loser Reihe weitere Erkenntnisse die ich beim Experimentieren mit PEX gewinne hier veröffentlichen. Heute möchte ich mich mit Overflow-Exceptions beschäftigen. Anlass war eine Diskussion mit Nico. Als erstes Beispiel wollen wir uns mal folgenden Code ansehen:

   1: public decimal Calc(decimal Value, bool Increase)
   2: {
   3:     if (Increase)
   4:         return Value+1;
   5:     else
   6:         return Value-1;
   7: }

Für eine komplette Codeabdeckung muss PEX eigentlich nur den Boolean-Parameter Increase berücksichtigen. Der Parameter Value kann eigentlich beliebig gewählt werden, auf die Code-Abdeckung hat der keinen Einfluss. Aber natürlich erkennt man dass es natürlich auch Fälle gibt in denen die Methode eine OverflowException wirft, nämlich bei value = decimal.MaxValue, Increase = true und value = decimal.MinValue, Increase = false.

Das erfreuliche ist, dass mit der aktuellen Version (0.9.40105.0) PEX zusätzlich zur Codeabdeckung auch Grenzwerte berücksichtigt:

image

Bei einem anderen Beispiel funktioniert das leider (noch) nicht.

   1: public decimal ToDecimal(double Value)
   2: {
   3:     return (decimal)Value;
   4: }

Hier wird leider keine Grenzwertbetrachtung gemacht, die ja zu einer Exception führen würde da der Wertebereich von decimal kleiner ist als der von double.

image

Thursday, February 19, 2009 8:54:48 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |   | 
 Wednesday, February 18, 2009

Bei Unit-Tests bietet die Code-Coverage eine gute und einfache Möglichkeit zu prüfen, ob durch die definierten Test-Cases alle Code-Pfade in einer Methode wirklich durch Tests ausgeführt werden. Dies hilft beispielsweise dabei, noch fehlende Test-Cases zu identifizieren.

Auch mit manuellen Tests ist es möglich, eine Code-Coverage Analyse durchzuführen um auch hier fehlende Test-Cases zu ergänzen. Das folgende Beispiel zeigt eine mögliche Vorgehensweise.

Zunächst wird davon ausgegangen, dass ein maueller Test spezifiziert ist. Dieser kann z.B. so aussehen:

image

Nun wird ein Unit-Test erzeugt. Im Unit-Test wird eine Test-Methode angelegt die der Main-Methode der zu testenden Anwendung entspricht:

   1: [TestMethod()]
   2: [STAThread]
   3: public void FrmMainConstructorTest()
   4: {
   5:     Application.EnableVisualStyles();
   6:     Application.SetCompatibleTextRenderingDefault(false);
   7:     Application.Run(new FrmMain());
   8: }

 

Anschließend muss geprüft werden, ob die Code Coverage analyse für die gewünschten Assemblies aktiviert ist (Menü Test / Edit Test Run Configurations / Local Test Run)

image

Wird nun der Test gestartet, öffnet sich die Testanwendung. Hier werden nun die einzelnen Test-Schritte gemäß Testspezifikation ausgeführt. Anschließend wird die Test-Anwendung beendet. Dies schließt automatisch auch den Test ab. Nun cann die Code-Coverage ausgewertet werden:

image

Und natürlich lassen sich auch die durchlaufenen und nicht durchlaufenen Code-Zeilen farblich kennzeichnen.

image

Den ganzen Ablauf wird in folgendem Video auch nochmals detailliert gezeigt:

Wednesday, February 18, 2009 11:02:18 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [2]    |  |   | 
Copyright © 2010 Thomas. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.
Pick a theme: