Ich selbst habe PEX das erste Mal auf der PDC 2008 kennengelernt. PEX ist die Abkürzung für Programming EXplorations und ist ein Projekt von Microsoft Research.

Wo kann PEX helfen? Schreibt man Unit Tests so macht man dies in erster Linie um das Objektmodell zu formen, um gängige Abläufe abzubilden und um besondere Fehlererwartungen zu testen. Hat man sich eine Testsuite erstellt so kann man jegliche Änderungen am Code schnell überprüfen und durch Fehlerhafte Tests ist das Feedback eindeutig wo die Probleme aufgetaucht sind.

Ob man nun allerdings den Code in seiner komplettheit testet sei mal dahingestellt. Mechanismen wie Code Coverage helfen einem dabei zu sehen wieviel Prozent der eigentlichen Code Basis getestet worden sind. Ein Wert von 80% ist eine gute Basis um den Code in die Versionsverwaltung einzuchecken. In der Regel ist es jedoch so, das die Unit Tests die man schreibt, oft nur die Ansätze wiederspiegeln, die man beim Design im Kopf hatte. Was noch fehlt ist das lästige suchen nach Fehlern wie Sie in der Regel durch ein “gutmütiges” Nutzen des Objektmodells nicht vorkommen. Die ideale Testabdeckung von Code ist 100%.

Genau hier kann PEX helfen. PEX analysiert den Code und such dediziert nach verschiedenen Fehlerquellen für jeden Programmpfad. Dabei werden die Eingabeparameter für Methoden mit teilweise exotischen Werten ausgestattet um Fehler zu produzieren auf die man normalerweise nicht kommt.

Wie funktioniert PEX?

PEX ist ein Addon für Visual Studio 2010. Für Visual Studio 2008 gibt es auch eine Version, auf Basis einer akademischen Lizenz.

Folgender Sourcecode als Basis:

   1: public bool Validate( CardType cardType, string creditCardNumber )
   2: {
   3:     //
   4:     // Fast path some validation
   5:     //
   6:     if ( creditCardNumber.Length > _maxLength )
   7:     {
   8:         return false;
   9:     }
  10:  
  11:     //
  12:     // Credit card to validate
  13:     //
  14:     int[] number = new int[ _maxLength ];
  15:  
  16:     int length = 0;
  17:     foreach ( char c in creditCardNumber.ToCharArray() )
  18:     {
  19:         if ( char.IsDigit( c ) )
  20:         {
  21:             number[ length++ ] = ( int ) c - '0';
  22:         }
  23:     }
  24:  
  25:     //
  26:     // Validate based on card type
  27:     //
  28:     switch ( cardType )
  29:     {
  30:         case CardType.MasterCard:
  31:             {
  32:                 if ( length != 16 ) 
  33:                     return false;
  34:                 if ( number[ 0 ] != 5 || number[ 1 ] == 0 || number[ 1 ] > 5 )
  35:                      return false;
  36:  
  37:                 break;
  38:             }
  39:  
  40:         case CardType.BankCard:
  41:             {
  42:                 if ( length != 16 ) 
  43:                     return false;
  44:                 if ( number[ 0 ] != 5 || number[ 1 ] != 6 || number[ 2 ] > 1 ) 
  45:                     return false;
  46:  
  47:                 break;
  48:             }
  49:  
  50:         case CardType.Visa:
  51:             {
  52:                 if ( length != 16 && length != 13 ) 
  53:                     return false;
  54:                 if ( number[ 0 ] != 4 ) 
  55:                     return false;
  56:  
  57:                 break;
  58:             }
  59:  
  60:         case CardType.AMEX:
  61:             {
  62:                 if ( length != 15 ) 
  63:                     return false;
  64:                 if ( number[ 0 ] != 3 || number[ 1 ] != 4 ) 
  65:                     return false;
  66:  
  67:                 break;
  68:             }
  69:  
  70:         case CardType.Discover:
  71:             {
  72:                 if ( length != 16 ) 
  73:                     return false;
  74:                 if ( number[ 0 ] != 6 || number[ 1 ] != 0 
  75:                     || number[ 2 ] != 1 || number[ 3 ] != 1 ) 
  76:                     return false;
  77:  
  78:                 break;
  79:             }
  80:  
  81:         case CardType.DinersClub:
  82:             {
  83:                 if (length != 14) 
  84:                     return false;
  85:                 if (number[0] != 3 || number[1] != 0 
  86:                     && number[1] != 6 && number[1] != 8) 
  87:                     return false;
  88:  
  89:                 break;
  90:             }
  91:  
  92:         case CardType.JCB:
  93:             {
  94:                 if ( length != 16 && length != 15 ) 
  95:                     return false;
  96:                 if ( number[ 0 ] != 3 ) 
  97:                     return false;
  98:  
  99:                 break;
 100:             }
 101:     }
 102:  
 103:     //
 104:     // Use the Luhn algorithm to validate the integrity of the number itself
 105:     //
 106:     int sum = 0;
 107:  
 108:     for ( int i = length - 1; i >= 0; i-- )
 109:     {
 110:         if ( i % 2 == length % 2 )
 111:         {
 112:             int n = number[ i ] * 2;
 113:  
 114:             sum += ( n / 10 ) + ( n % 10 );
 115:         }
 116:         else
 117:         {
 118:             sum += number[ i ];
 119:         }
 120:     }
 121:  
 122:     return ( sum % 10 ) == 0;
 123: }

Und der CardType Enum

   1: public enum CardType
   2: {
   3:     MasterCard = 1,
   4:     BankCard = 2,
   5:     Visa = 3,
   6:     AMEX = 4,
   7:     Discover = 5,
   8:     DinersClub = 6,
   9:     JCB = 7,
  10: }

Diese Routine möchte ich nun mit Unit Tests ausstatten. Ich habe welche in meinem Projekt und erreiche eine Abdeckung über Data-Driven Tests von ca. 67%, noch einige Prozentpunke entfernt von dem selbstgesetzten Ziel von 80%.

Nun benutze ich PEX um den Code zu analysieren und mir eine Test-Suite zu erstellen und dabei auch noch Fehler zu finden die ich vielleicht übersehen habe und nicht Teil meiner bisherigen Test-Suite sind.

Dazu rufe ich PEX über das Kontext-Menü im Source Editor auf:

shot1

Danach startet PEX einen Prozess und instanziert die zu testenden Klassen und ruft systematisch in die einzelnen Methoden rein.

shot2

Das Ergebnis sind 262 Aufrufe mit einem gefunden Fehler und 40 erstellten Unit Tests.

shot3

shot4

Und zur Komplettheit noch der generierte Unit Test der für diesen Fehlerfall erzeugt wurde.

   1: [TestMethod]
   2: [PexRaisedException(typeof(NullReferenceException))]
   3: [PexGeneratedBy(typeof(LuhnValidatorTest))]
   4: public void Validate02()
   5: {
   6:     LuhnValidator luhnValidator;
   7:     bool b;
   8:     luhnValidator = new LuhnValidator();
   9:     b = this.Validate(luhnValidator, CardType.BankCard, (string)null);
  10: }

Des Weiteren gibt PEX einem die Möglichkeit , gleich eine Pre-Condition in den Code einzubauen die die Kreditkartennummer auf null prüft und eine ArgumentNullException wirft.

shot5

Und mein Ziel 80% Code Coverage zu erreichen?

Die Tests lassen sich abspeichern und entsprechend in die dafür vorgesehene BVT Suite (BVT = Build Verification Tests) integrieren.

Und nach einem weiteren Testlauf im Test View von Visual Studio 2008 mit aktivierter Code Coverage erscheint nun folgendes Resultat für die Validate Methode:

shot6

Wow, 100%, mehr als ich wollte. Ein Tool das einen Blick Wert ist!

Die Webseite von PEX und die entsprechenden Downloads findet man hier.