Blog Home  Home Feed your aggregator (RSS 2.0)  
artiso Blog - Qualitätsmanagement
Neues rund um's Thema .Net
 
 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]    |  |  |   | 
 Friday, March 13, 2009

MSDN Webcasts

Am 16.03.09 geht ein neuer Webcast von mir online mit dem Thema "Team Build mit Custom Build Tasks erweitern". Ich möchte hier schon mal die entsprechenden Infos veröffentlichen.

In TeamBuild lassen sich eigene Build-Task integrieren. Diese können sehr einfach erstellt werden. Hierzu wird eine Klasse erstellt und diese von Task abgeleitet. Im folgenden Beispiel wird ein Build-Task erstellt, der im Rahmen des Builds die Version der Anwendung setzt. Und zwar soll hier die Build-Nummer in der Versionsnummer der Assembly abgebildet werden. Dies bietet Vorteile, wenn zu einer bestimmten Anwendungsversion der entsprechende Build identifiziert werden soll.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using Microsoft.Build.Utilities;
   5: using Microsoft.Build.Framework;
   6:  
   7: namespace Artiso.BuildTasks
   8: {
   9:     /// <summary>
  10:     /// Creates a AssemblyVersion out of a BuildNumber
  11:     /// </summary>
  12:     /// <remarks>
  13:     /// AssemblyVersion.Minjor and AssemblyVersion.Minor will be defined fiexed in
  14:     /// the Build-Script. If the BuildNumber is Dev_Versioning_20090305.4 we use 
  15:     /// two digit year and month for AssemblyBuildNumber and day and 3 digit BuildRevision
  16:     /// for AssemblyRevision. BuildRevisio 
  17:     /// </remarks>
  18:     /// <example>
  19:     /// Dev_Versioning_20090305.4 => xx.yy.0903.05004
  20:     /// </example>
  21:     public class ExtractRevision : Task
  22:     {
  23:         #region [rgn] Fields(3)
  24:         private string buildRevision;
  25:         private string buildVersion;
  26:         private string buildNumber;
  27:         #endregion
  28:  
  29:         #region [rgn] Properties(3)
  30:         /// <summary>
  31:         /// Input Build Number
  32:         /// </summary>
  33:         [Required]
  34:         public string BuildNumber
  35:         {
  36:             set { buildNumber = value; }
  37:         }
  38:  
  39:         /// <summary>
  40:         /// Returns the sortened date of the build
  41:         /// </summary>
  42:         [Output]
  43:         public string BuildVersion
  44:         {
  45:             get { return buildVersion; }
  46:         }
  47:  
  48:         /// <summary>
  49:         /// Returns the Build Revision (number of build at this day
  50:         /// </summary>
  51:         [Output]
  52:         public string BuildRevision
  53:         {
  54:             get { return buildRevision; }
  55:         }
  56:         #endregion
  57:  
  58:         #region [rgn] Methods(1)
  59:         public override bool Execute()
  60:         {
  61:             buildVersion = "0";
  62:             buildRevision = "0";
  63:  
  64:             if (buildNumber != null && buildNumber.Contains("."))
  65:             {
  66:                 string[] buildNumberParts = buildNumber.Substring(buildNumber.LastIndexOf('_')+1).Split('.');
  67:  
  68:                 // Dev_Versioning_20090305.4 -> 0903.02005
  69:                 // use year (2 digits) and mont for buildversion
  70:                 buildVersion = buildNumberParts[0].Substring(2, 4);
  71:  
  72:                 // use day and number of build in this day for build revision
  73:                 buildRevision = buildNumberParts[0].Substring(6) + buildNumberParts[1].PadLeft(3, '0');
  74:             }
  75:             return true;
  76:         }
  77:         #endregion
  78:     }
  79: }

Das Property BuildNumber wird als Input-Parameter verwendet und mit dem Attribut [Required] versehen. Darüber kann die BuildNumber in unseren Task übergeben werden. Durch das Attribut [Output] werden die beiden Properties BuildVersion und BuildRevision als Rückgabewerte definiert. Beim Ausführen des Builds wird die Methode Execute aufgerufen. Hier werden nun aus der Build-Number die entsprechenden Informationen extrahiert und diese dann in BuildVersion und BuildRevision zurückgeschrieben. Dies ist natürlich nur ein einfaches Beispiel, aber mit diesen Grundlagen können nun beliebige Build-Tasks definiert werden. Im nächsten Schritt schauen wir uns an, wie wir diesen Custom Build Task nun in unseren Build-Prozess einbinden. Hierzu kopieren wir zunächst die Assembly in einen entsprechenden Ordner. Hier bietet sich an unter c:\Program Files\MSBuild einen entsprechenden Ordner anzulegen und dort die Assemblies abzulegen.

Nun muss das Build-Script entsprechend angepasst werden. Diese liegt in der Quellcode-Verwaltung und muss zum Bearbeiten zuerst aus- und danach wieder eingechecked werden. Um diesen Vorgang zu vereinfachen empfehle ich die TFS Sidekicks (http://www.attrice.info/downloads/index.htm) die direkt im Kontextmenü des TeamExplorers entsprechende Kommandos einfügt. Das nun ausgecheckte PROJ-File kann nun bearbeitet werden.

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <!-- DO NOT EDIT the project element - the ToolsVersion specified here does not prevent the solutions 
   3:      and projects in the SolutionToBuild item group from targeting other versions of the .NET framework. 
   4:      -->
   5: <Project DefaultTargets="DesktopBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
   6:  
   7:   <!-- Do not edit this -->
   8:   <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
   9:   <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
  10:  
  11:   <UsingTask AssemblyFile="$(MSBuildExtensionsPath)\ArtisoBuildTasks\ArtisoBuildTasks.dll"
  12:     TaskName="ExtractRevision"/>
  13:  
  14:   <PropertyGroup>
  15:     <!-- Assembly version properties. Adjust here Major and Minor Version-->
  16:     <AssemblyMajorVersion>1</AssemblyMajorVersion>
  17:     <AssemblyMinorVersion>3</AssemblyMinorVersion>
  18:     <AssemblyBuildNumber>1</AssemblyBuildNumber>
  19:     <AssemblyRevision>1</AssemblyRevision>
  20:   </PropertyGroup>
  21:  
...
In Zeile 11 wird unser BuildTask entsprechend registriert. In Zeile 9 werden noch weitere Build-Tasks registriert. Hier gereicht es uns zum Vorteil, dass Team-Build auf MSBuild basiert. D.h. es können entsprechende Tasks für MSBuild problemlos integriert werden. Diese gibt es in großer Zahl für sehr viele Anwendungsbereiche zum großen Teil frei Verfügbar zum Download. Wir verwenden hier die MSBuild Community Tasks (http://msbuildtasks.tigris.org/). Wir werden aus diesem Paket Aktionen verwenden.
 
In den Zeilen 14 bis 20 wird eine sog. PropertyGroup angelegt. Darin werden einzelne Properties definiert und mit Default-Werten vorbelegt. Diese Properties lassen sich mit Variablen innerhalb eines Software-Codes vergleichen. Die AssemblyMajorVersion und AssemblyMinorVersion werden hier festgelegt. AssemblyBuildNumber und AssemblyRevision werden wir im weiteren Verlauf überschreiben.
 
Am Ende des Scripts direkt vor dem schließenden </Project>-Tag wird nun ein Target-Block eingefügt.
 
   1: ...
   2: <Target Name="AfterGet">
   3:    <ItemGroup>
   4:      <AssemblyInfoFiles Include="$(SolutionRoot)\**\assemblyinfo.cs" />
   5:    </ItemGroup>
   6:  
   7:    <Message Text="Get Revision Number from BuildNumber &quot;$(BuildNumber)&quot;." />
   8:    <ExtractRevision BuildNumber="$(BuildNumber)">
   9:      <Output TaskParameter="BuildRevision" 
  10:              PropertyName="AssemblyRevision" />
  11:      <Output TaskParameter="BuildVersion" 
  12:              PropertyName="AssemblyBuildNumber" />
  13:    </ExtractRevision>
  14:  
  15:    <!-- Update all the assembly info files with generated version info -->
  16:    <Message Text="Modifying AssemblyInfo files under &quot;$(SolutionRoot)&quot;." />
  17:    <Attrib Files="@(AssemblyInfoFiles)" Normal="true" />
  18:    <FileUpdate Files="@(AssemblyInfoFiles)"
  19:                Regex="AssemblyVersion\(&quot;.*&quot;\)\]" 
  20:                ReplacementText="AssemblyVersion(&quot;$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision)&quot;)]" />
  21:    <FileUpdate Files="@(AssemblyInfoFiles)"
  22:                Regex="AssemblyFileVersion\(&quot;.*&quot;\)\]" 
  23:                ReplacementText="AssemblyFileVersion(&quot;$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision)&quot;)]" />
  24:    <Message Text="AssemblyInfo files updated to version &quot;$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision)&quot;" />
  25:  </Target>
  26:  
  27: </Project>
Über den Namen des Target-Blocks mit "AfterGet" wird festgelegt, dass dieser Block ausgeführt wird, nachdem der Build-Prozess die Quelldateien aus der Versionsverwaltung geholt hat. Genau zu diesem Zeitpunkt wollen wir unsere Versionierung anpassen. In den Zeilen 3 bis 5 erstellen wir eine ItemGroup die alle assemblyinfo.cs Dateien unserer Solution enthält. In diesen Dateien wollen wir die Version anpassen. In Zeile 7 wird eine Meldung in das Build-Log geschrieben. Dies ist hilfreich, um Fehler im Ablauf des Scriptes besser einordnen zu können.
In den Zeilen 8 bis 13 wird nun unser Build-Task aufgerufen. Wir übergeben die Buildnummer $(BuildNumber) in den Parameter BuildNumber und lesen die Output-Parameter aus und schreiben diese in AssemblyRevision bzw. AssemblyBuildNumber (die Properties die wir weiter oben definiert hatten). In Zeile 17 heben wir den Schreibschutz der AssemblyInfo-Dateien auf und in den folgenden Zeilen wird mit Hilfe eines Ersetzen-Vorgangs die Version in den AssemblyInfo-Dateien ersetzt. Für diese Aktionen nutzen wir die Community Build Tasks.
Damit können wir nun die Version unserer Anwendung bei jedem Build entsprechend setzen.
In einem nächsten Schritt wollen wir den Build-Task nun noch erweitern um das Build-Result in einer ZIP-Datei zu verpacken und diese anschließend per Mail zu versenden. Auch hierbei greifen wir auf die MSBuild Community Tasks zurück. Das entsprechende Target-Tag fügen wir einfach nach dem zuvor definierten ein. Als Name geben wir "AfterCompile" an so dass diese Aktionen nach dem Kompilieren ausgeführt werden.
   1: <Target Name="AfterCompile">
   2:   <CreateItem Include="..\Binaries\Release\**\*.*" Exclude="..\Binaries\Release\**\*.pdb;..\Binaries\Release\**\*codeanalysis*">
   3:     <Output ItemName="ZipFiles" TaskParameter="Include"/>
   4:   </CreateItem>
   5:  
   6:   <Message Text="Zipping Buildresult to \\tfs\deploy\BuildDemo\BuildDemo_$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision).zip" />
   7:  
   8:   <Zip ZipFileName="\\tfs\deploy\BuildDemo\BuildDemo_$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision).zip" 
   9:        Files="@(ZipFiles)"
  10:        WorkingDirectory="..\Binaries\Release\"/>
  11:  
  12:   <Mail SmtpServer="tfs"
  13:           To="tschissler@tfs"
  14:           From="build@tfs"
  15:           Subject="BuildDemo v$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision) released"
  16:           Body="A new version of the BuildDemo was released. Please find the newest files attached to this mail. You can also download them from the download folder."
  17:           Attachments="\\tfs\deploy\BuildDemo\BuildDemo_$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision).zip"/>
  18: </Target>

Hier sammeln zunächst alle Dateien aus dem Build-Drop-Verzeichnis exklusive der PDB- und Codeanalyse-Dateien In Zeilen 8-10 werden diese Dateien in ein ZIP-File verpackt dem wir im datei-Name die Version mitgeben. Anschließend versenden wir eine e-Mail der wir dieses ZIP-File als Attachment anhängen.

Als zweite Variante wollen wir im Rahmen des Builds ein Click-Once Paket erstellen. Die Herausforderung bei der Erstellung des ClickOnce-Paketes ist dass dort die Deployment-Url hinterlegt werden muss. Vor allem wenn verschiedene Pakete für unterschiedliche Kunden erstellt werden sollen, ist dies nur durch eine Automatisierung im Rahmen des Builds sinnvoll handelbar. Hierzu ersetzen wir den AfterCompile-Target durch folgendes Script:

   1: <Target Name="AfterCompile">
   2:    <!-- Publish using ClickOnce -->
   3:    <Message Text="modify Publish directory for $(SolutionRoot)" />
   4:    <!-- Update directory where to publish the project -->
   5:    <ItemGroup>
   6:      <ProjectFiles Include="$(SolutionRoot)\Source\Dev\BuildDemo\BuildDemo.csproj" />
   7:    </ItemGroup>
   8:    <PropertyGroup>
   9:      <PublishDir>\\tfs\Deploy\BuildDemo\ClickOnce\</PublishDir>
  10:      <InstallUrl>\\tfs\Deploy\BuildDemo\ClickOnce\</InstallUrl>
  11:    </PropertyGroup>
  12:    <Attrib Files="@(ProjectFiles)" Normal="true" />
  13:    <FileUpdate Files="@(ProjectFiles)"
  14:      Regex="&lt;PublishUrl&gt;.*&lt;/PublishUrl&gt;"
  15:        ReplacementText="&lt;PublishUrl&gt;$(PublishDir)&lt;/PublishUrl&gt;" />
  16:    <FileUpdate Files="@(ProjectFiles)"
  17:        Regex="&lt;InstallUrl&gt;.*&lt;/InstallUrl&gt;"
  18:        ReplacementText="&lt;InstallUrl&gt;$(InstallURL)&lt;/InstallUrl&gt;" />
  19:  
  20:    <MSBuild Projects="@(ProjectFiles)"
  21:    Properties="PublishDir=$(PublishDir);ApplicationVersion=$(AssemblyMajorVersion).$(AssemblyMinorVersion).$(AssemblyBuildNumber).$(AssemblyRevision)"
  22:            Targets="Publish" />
  23:  
  24:  </Target>

In den Zeilen 5 bis 7 lesen wir das csproj-File der Anwendung in eine ItemGroup. Anschließend definieren wir zwei Properties für PublishDir und InstallUrl. Diese werden dann über eine Ersetzung in die csproj-Datei eingefügt. Anschließend wird ein MSBuild-Task gestartet der das ClickOnce-Paket erstellt und an der angegebenen PublishDir und mit der Versionsnummer veröffentlicht.

Das Ganze wird in dem genannten Webcast Live demonstriert. Über Feedback würde ich mich sehr freuen.

Details zur Veranstaltung: Team Build mit Custom Build Tasks erweitern [1032405249] - Microsoft Deutschland GmbH

Friday, March 13, 2009 2:05:16 AM (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

Hier hatte ich beschrieben, wie man die Test-Results aus Visual Studio nach Excel übertragen kann. Heute möchte ich ein Tool vorstellen, das es erlaubt, TRX-Files nach HTML zu konvertieren.

Zunächst speichert man das Test-Result in ein TRX-File. Dies geht über den Button "Export Test Run Results".

image

Anschließend kann man mit dem Tool trx2html das HTML-File erstellen. trx2html ist ein Open-Source Projekt von CodePlex. Es wird als Commandozeilen-Tool ausgeführt und als Parameter wird einfach das TRX-File angegeben. Als Ergebnis wird nun ein HTML-File erstellt das die Testergebnis entsprechend dokumentiert und auch einige Drill-Down-Funktionen bietet:

image

Durch einen Klick auf das rote Kreis-Icon neben einem der Test kann z.B. ein Stack-Trace eingeblendet werden.

image

trx2html - Home

Thursday, February 19, 2009 12:43:08 AM (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]    |  |   | 
 Wednesday, January 14, 2009

Endlich gibt es von PEX nun auch eine kommerziell nutzbare Lizenz. Bisher stand nur eine Academic License zur Verfügung, mit der nur “gespielt” werden durfte. Aber welche Aussagekraft hat denn eine Bewertung des Tools mit Demo-Projekten? Jetzt kann man PEX nun endlich auch auf Real-World-Projekte loslassen.

http://research.microsoft.com/en-us/projects/pex/downloads.aspx

Wednesday, January 14, 2009 12:05:22 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    | 
 Tuesday, October 14, 2008

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.

image

Im folgenden Dialog können Sie verschiedene Parameter angeben. Das wichtigste hier ist das Testprojekt in dem der Stub erzeugt werden soll.

image

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.

image

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.

image

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.

image

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.
 

image

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)
   4:     {
   5:         if (Input.StartsWith("abc") && Input.Length > 10)
   6:             return Input + " Starts with 'abc' and length > 10"; 
   7:         if (Input.StartsWith("abc"))
   8:             return Input + " Starts with 'abc'";
   9:         if (Input.StartsWith("ABC"))
  10:             return Input + " Starts with 'ABC'";
  11:         return "Unknown pattern";
  12:     }
  13: }

Stubs erzeugen und Exploration starten. Ob PEX wohl solche Operationen wie "StartsWith" und "Length" versteht?

image

Es findet tatsächlich alle erforderlichen Input-Parameter und sogar noch mehr! PEX stell fest, dass unsere Methode beim Übergeben einer NULL-Referenz eine Exception wirft. Und damit nicht genug, PEX kann uns auch einen Vorschlag machen, wie wir unseren Code verbessern können. Dazu im "Pex Exploration Results" - Fenster unter Views "Show suggestions window" aufrufen.

image

image

Durch einen Doppelklick auf den Eintrag am unteren Rand des Bereichs öffnet sich ein Fenster, das die vorgeschlagene Änderung direkt in unseren Code einfügen kann.

image

   1: public string CheckString(string Input)
   2: {
   3:     // <pex>
   4:     if (Input == (string)null)
   5:         throw new ArgumentNullException("Input");
   6:     // </pex>
   7:     if (Input.StartsWith("abc") && Input.Length > 10)
   8:         return Input + " Starts with 'abc' and length > 10"; 
   9:     if (Input.StartsWith("abc"))
  10:         return Input + " Starts with 'abc'";
  11:     if (Input.StartsWith("ABC"))
  12:         return Input + " Starts with 'ABC'";
  13:     return "Unknown pattern";
  14: }

Die Zeilen 3-6 wurden von PEX erzeugt. Natürlich können wir das entsprechende Verhalten im Code direkt ändern und an unsere Vorstellungen anpassen. Vielleicht ist es aber gar keine schlechte Idee, in diesem Fall eine Exception zu werden. Dies ist das Standard-Verhalten von PEX an dieser Stelle. Wird die erwartete Exception nicht mehr geworfen oder eine andere Exception tritt auf, wird dies durch einen fehlgeschlagenen Test angezeigt.

Der nächste bitte!

So nun wollen wir noch einen Schritt weitergehen und sehen, wie PEX mit Listen umgehen kann. Dazu habe ich folgende Testmethode erstellt (über den Sinn einer solchen Methode wollen wir jetzt nicht nachdenken)

   1: int result = 0;
   2: if (list.Count > 10)
   3: {
   4:     foreach (int i in list)
   5:         result += i;
   6: }
   7: else
   8: {
   9:     foreach (int i in list)
  10:         result *= i;
  11: }
  12: return result;

Die Methode bekommt eine Liste von Integer-Werten übergeben. Wenn es mehr als 10 Elemente sind, werden diese addiert, sonst werden die Werte miteinander Multipliziert. Mal sehen, wie PEX mit Listen umgeht.

image

PEX erkennt noch, dass unsere Methode mit Null-References nicht korrekt umgeht, aber dann verließen sie ihn.  Aber freundlicherweise bekommen wir noch einen Hinweis "2 Object Creations". klickt man darauf, dann bekommt man schon mehr Informationen.

image

Aha, PEX kann also eine List<int> nicht erzeugen. Also was tun? Klickt man den unteren der beiden Einträge an, bietet PEX etwas weiter rechts die Möglichkeit eine Factory zu definieren. Eine Factory ist ein Extensibility-Point mit dem PEX beigebracht werden kann mit solchen Objekten umzugehen. Ein Beispiel für eine solche Factory kann so aussehen:

   1: namespace System.Collections.Generic
   2: {
   3:     [PexFactoryClass]
   4:     public partial class ListFactory
   5:     {
   6:         [PexFactoryMethod(typeof(List<int>))]
   7:         public static List<int> Create(int i)
   8:         {
   9:             if (i > 100)
  10:                 i = 100;
  11:             List<int> l = new List<int>();
  12:             for (int j = 0; j < i; j++)
  13:             {
  14:                 l.Add(j * 10);
  15:             }
  16:             return l;
  17:         }
  18:     }
  19: }

Hier teilt man PEX nun mit, welche Elemente es damit erzeugen kann (Zeile 6). Anschließend implementiert man eine Create-Methode die beliebige Parameter übernehmen kann. In Abhängigkeit dieser Parameter wird nun eine Instanz des gewünschten Objektes erzeugt. In unserem Beispiel übernehmen wir nur einen Parameter der die Länge der Liste angibt. In den Zeilen 9/10 begrenzen wir die Länge der Lsite auf 100 Elemente. Die Liste selbst befüllen wir mit einer Reihe von Zahlen. Hier ist es sicher keine gute Idee, z.B. Zufallszahlen zu verwenden, da diese ja bei jedem Testdurchlauf andere Werte liefern und deshalb der Assert nicht erfolgreich ausgeführt werden kann.

Mit hilfe dieser Factory kannPEX nun unsere Testcases definieren. Dazu ermittelt es einfach geeignete Parameter für die Create-Methode in unserer Factory statt das Objekt selbst zu erzeugen.

image

Die erzeugten Tests sehen dann so aus:

   1: [TestMethod]
   2:         [PexGeneratedBy(typeof(ListCalculationTest))]
   3:         public void SumList03()
   4:         {
   5:             List<int> list;
   6:             int i;
   7:             list = ListFactory.Create(2);
   8:             ListCalculation listCalculation = new ListCalculation();
   9:             i = this.SumList(listCalculation, list);
  10:             Assert.AreEqual<int>(0, i);
  11:         }
  12:  
  13:         [TestMethod]
  14:         [PexGeneratedBy(typeof(ListCalculationTest))]
  15:         public void SumList04()
  16:         {
  17:             List<int> list;
  18:             int i;
  19:             list = ListFactory.Create(536870912);
  20:             ListCalculation listCalculation = new ListCalculation();
  21:             i = this.SumList(listCalculation, list);
  22:             Assert.AreEqual<int>(49500, i);
  23:         }

 

Nun folgt noch die Kür

Nach den Erfahrungen mit der Liste bereits etwas skeptischer geworden, wollen wir's jetzt aber doch wissen. Wie sieht's mit eigenen Objekten aus? Wie weit kommt PEX damit? Auch hier gibt es wieder eine einfache Methode die wir testen wollen:

   1: public class ComplexDataCalculation
   2: {
   3:     public string DoComplexDataCalculation(cData d)
   4:     {
   5:        if (!d.IsValid)
   6:             return "NotValid";
   7:  
   8:         if (d.DataValues.x1 > d.DataValues.x2)
   9:             return "x1 > x2";
  10:         if (d.DataValues.x1 < d.DataValues.x2)
  11:             return "x1 < x2";           
  12:         else
  13:             return "x1 == x2";
  14:  
  15:     }
  16: }
  17:  
  18: public class cData
  19: {
  20:     public cDataValues DataValues { get; set; }
  21:     public string ObsoleteParameter { get; set; }
  22:     public bool IsValid { get; set; }
  23: }
  24:  
  25: public class cDataValues
  26: {
  27:     public int x1 { get; set; }
  28:     public int x2 { get; set; }
  29: }

Die Methode bekommt einen komplexen Datentyp übergeben. Und was macht PEX???

image

PEX erkennt nicht nur, dass wir zwei Null-Exceptions abfangen sollten, sondern kann auch unser Datenobjekt so initialisieren, dass wir wieder eine 100% Codeabdeckung bekommen. Die Unit-Tests sehen ungefähr so aus:

   1: [TestMethod]
   2: [PexGeneratedBy(typeof(ComplexDataCalculationTest))]
   3: public void DoComplexDataCalculation04()
   4: {
   5:     cDataValues cDataValues;
   6:     cData cData;
   7:     string s;
   8:     cDataValues = new cDataValues();
   9:     cDataValues.x1 = 3;
  10:     cDataValues.x2 = 4;
  11:     cData = new cData();
  12:     cData.DataValues = cDataValues;
  13:     cData.ObsoleteParameter = "";
  14:     cData.IsValid = true;
  15:     ComplexDataCalculation complexDataCalculation = new ComplexDataCalculation();
  16:     s = this.DoComplexDataCalculation(complexDataCalculation, cData);
  17:     Assert.AreEqual<string>("x1 < x2", s);
  18: }

Das ist schon extrem beeindruckend, wie PEX tatsächlich eine entsprechende Instanz unseres Datenobjektes erzeugt und initialisiert und das so, dass wir alle Testcases abdecken. Wirklich beeindruckend.

Was haltet ihr davon? Würde mich über Feedback freuen.

Tuesday, October 14, 2008 1:10:10 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [2]    |  |  |   | 
 Thursday, July 24, 2008

MSDN Webcasts

Zusammen mit Christian Binder habe ich nun einen MSDN Webcast aufgenommen der sich mit dem Thema UI Events und Record & Play für UI Testing beschäftigt. Dieser Webcast ist als Fortsetzung zum Thema UI Testing mit dem UI Automation Framework aufgebaut. Der Webcast selbst wird als Download ab dem 08.08.08 (cooles Datum) verfügbar sein. Den Demo_Code zum Webcast kann man ab sofort hier runterladen.

Kurz hier der Inhalt des Webcasts zusammengefasst.

Über das UI Automation Framework kann man Events aus einer Anwendung abfangen. Diese Events kann man zunächst am besten mit dem UISpy untersuchen. Nun kann man einen kleinen Recorder erstellen, der diese Events nutzt, um Benutzereingaben in einer Anwendung zu erkennen und automatisiert Code erstellt, der diese Benutzereingaben simuliert. Dieser Code kann nun in einem Unit-Test genutzt werden um automatisiert im Rahmen eines Testdurchlaufes die Benutzerinteraktion zu wiederholen. Der Clou dabei ist, dass sogar die Asserts für die erwarteten Ergebnisse automatisiert aufgezeichnet werden können.

 

Download Demo-Code 

Download Webcast

Thursday, July 24, 2008 9:18:50 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |  |   | 
 Monday, June 09, 2008

Zum Aufzeichnen von WebTests mit VSTS wird ein Web Test Recorder Toolbar im IE integriert.

image 

Unter meinem Vista 64 Bit hatte ich allerdings das Problem, dass der Toolbar dort partou nicht angezeigt wurde. Nach einigem Suchen habe ich dann die Lösung gefunden:

VSTS 2008 : Vista (64 bit) : Recorder bar does not appear when recording a new webtest

Fix:  Vista caches the list of explorer bars you have available and the recorder bar was not included in your list.  The fix is to force Windows to rebuild that cache.  To do this, first make sure you have all Internet Explorer instances shut down, then open the 32 bit registry editor and delete the following keys:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Discardable\PostSetup\Component Categories\{00021493-0000-0000-C000-000000000046}
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Discardable\PostSetup\Component Categories\{00021494-0000-0000-C000-000000000046}

[Note: by default, the 32 bit registry editor is located in %WINDIR%\SysWow64\RegEdt32.exe]

The next time you boot Internet Explorer, your explorer bar cache will be rebuilt and the recorder bar should be available.

Nachdem ich diese beiden Registry-Keys gelöscht habe, wurde der Toolbar angezeigt.

Weitere Infos und tipps bei Probleme mit dem Web Recoder Toolbar finden sich unter Michael Taute's Blog : Diagnosing and fixing Web Test recorder bar issues.

Monday, June 09, 2008 8:57:40 PM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |   | 
 Thursday, March 20, 2008

Weitgehend unbemerkt von der breiten Masse der Entwickler bringt das .net Framework 3.0 auch eine Bibliothek zum Erstellen von automatisierten UI-Tests mit. Unter %PROGRAMFILES%\Reference Assemblies\Microsoft\Framework\v3.0 finden sich folgen sich die benötigten DLLs. Damit kann man UI-Tests selbst programmieren und auch in Unit-Tests integrieren. Das entspricht zwar nicht der weit verbreiteten Erwartungshaltung, die eher von einer "Record & Play" Methode ausgehen, bietet aber verschiedene Vorteile in Bezug auf Wartbarkeit und Stabilität.

Ich habe hier mal ein kleines Beispiel gebaut, das den Einsatz demonstriert:

Ich werde in Kürze weitere Details bloggen und verschiedene Einsatzgebiete, Erweiterungen und auch Grenzen aufzeigen. Auch ein Webcast ist zu dem Thema geplant. Bis dahin erst mal ein paar weiterführende Dokumente. Leider ist da im Moment noch nicht sehr viel publiziert worden.

http://msdn2.microsoft.com/en-us/magazine/cc163288.aspx
http://msdn2.microsoft.com/en-us/accessibility/bb892133.aspx

Das Tool ist auf jeden Fall interessant und da es kostenlos mit dem .net Framework mitkommt, sollte jeder der sich mit Software-Tests beschäftigt mal einen Blick drauf werfen.

Thursday, March 20, 2008 12:45:12 AM (Mitteleuropäische Zeit, UTC+01:00)  #    Comments [0]    |  |   | 
Copyright © 2010 Thomas. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.
Pick a theme: