Code-Coverage bei generiertem Code mit T4 korrigieren

29. April 2013

div class=”articleAbstract”>Ich möchte Ihnen mit diesem Artikel zeigen, wie ich mit Hilfe des Text Templating Transformation Toolkit (T4) durch einen Codegenerator erzeugte Klassen aus der Code-Coverage (Testabdeckung)-Analyse ausschließe. Ich erreiche somit, dass die Analyse nur den von mir verfassten Code analysiert und ich erhalte einen aussagekräftigen Status der Testabdeckung.

Die Ausgangslage

In einem aktuellen Projekt verwende ich einen Codegenerator, der das Datenmodell einer Dynamics CRM-Implementierung und den Datenkontext zur Kommunikation mit dieser generiert. Der Codegenerator (crmsvcutil.exe) ist Bestandteil des Microsoft Dynamics CRM 2011 Software Developer Kit (SDK).
Der mit dem Codegenerator erzeugte Code besteht aus knapp 170.000(!) Zeilen Code, sodass es bei der Messung der Code-Coverage nicht verwunderlich ist, dass diese nur sehr klein ist (einstelliger Prozentsatz).


Resultat der CodeCoverage mit generiertem Code

[ExcludeFromCodeCoverage] als Lösung

Mithilfe des „ExcludeFromCodeCoverage“-Attributs des Namespace System.Diagnostics.CodeAnalysis ist es möglich, Klassen aus der Code-Coverage-Analyse auszuschließen. Die Aufgabe ist jetzt, die durch den Codegenerator erstellten Klassen mit diesem Attribut zu erweitern. Es macht jedoch keinen Sinn, eigenen Code in die generierte C#-Datei (*.cs) zu schreiben, da dieser beim Aktualisieren durch den Generator ohne Rückfrage überschrieben wird.
Die autogenerierten Klassen sind alle als „partial” deklariert. Es ist also möglich eine C#-Datei mit entsprechenden „partial” Klassen zu erstellen, die alle das „ExcludeFromCodeCoverage”-Attribut erhalten. Da dies eine Überprüfung von 170.000 Zeilen Code bedeutet, entschied ich mich das T4 zu verwenden.

T4 im Einsatz

Ziel ist es, eine Vorlage zu erzeugen, anhand derer für jede generierte “partial“ Klasse Codezeilen nach folgendem Muster anlegen werden sollen:

   1: /// <summary>

   2: /// The class part for excluding the class from the code coverage analysis

   3: /// This part is auto generated. All changes done here will be overwritten.

   4: /// </summary>

   5: [ExcludeFromCodeCoverage]

   6: public partial class SomeAutoGeneratedClass

   7: {

   8: }

Dazu füge ich dem Projekt erst einmal eine Textvorlage hinzu. Mit dem Filter „T4“ bietet das Visual Studio im „Neues Element hinzufügen“-Dialog folgende Vorlagen an:

  • Laufzeit-Textvorlage: erzeugt eine Transformationsklasse, die Codeseitig angesprochen werden kann
  • Textvorlage: Vorlage wird manuell oder beim Speichern ausgeführt


Neues Element Dialog gefiltert auf T4

Für das Beispiel verwende ich die Textvorlage.
Am Anfang der Textvorlage befindet sich der Bereich für Direktiven (http://msdn.microsoft.com/en-us/library/bb126421.aspx). Folgende Direktiven habe ich in meinem Beispiel verwendet:

  • <#@ template … #>  debug=“true“: aktiviert die Codezeilenausgabe bei Fehlern  hostspecific=“true“: ermöglicht den Zugriff auf den IServiceProvider und über diese auf die Entwicklungsumgebung language=“C#“: (oder:“VB“) definiert die verwendete Programmiersprache
  • <#@ assembly name=“…“ #> lädt Assemblies, damit deren Klassen innerhalb der Vorlage codeseitig verwendet werden können
  • <#@ import namespace=“…“ #> erlaubt den Zugriff auf den angegebenen Namespace innerhalb der Vorlage
  • <#@ output … #> extension=“.cs“ oder “.vb“ definiert das Ausgaberesultat der Vorlage

Codezeilen, die durch den Compiler abgearbeitet werden sollen und die Programmlogik in C# oder VB enthalten, sind in folgender Syntax zu schreiben:

  • <# C#/VB Code #>
  • <#+ wiederverwendbarer C#/VB Code (Variablen, Methodendeklarationen) #>
  • <#= Variablenausgabe #>

Ich starte mit dem <# … #>-Block in dem ich eine Klasseninstanz der Entwicklungsumgebung lade. Dazu muss im Deklarationsbereich <#@ template hostspecific=“true“ … #> gesetzt sein.

   1: IServiceProvider hostServiceProvider = (IServiceProvider)Host;

   2: EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));

Als nächstes wird das Projekt nach dem Namespace durchsucht, den der Codegenerator erstellt hat und der die partiellen Klassen enthält.

   1: foreach (EnvDTE.ProjectItem projectItem in project.ProjectItems)

   2: {

   3:     EnvDTE.FileCodeModel fileCodeModel = projectItem.FileCodeModel;    

   4:     if(fileCodeModel != null)

   5:     {

   6:         foreach(EnvDTE.CodeElement codeElement in fileCodeModel.CodeElements)            

   7:         {

   8:             EnvDTE.CodeNamespace codeNamespace = codeElement as EnvDTE.CodeNamespace;           

   9:             if(codeNamespace == null || codeNamespace.FullName != gesuchterNamespace) 

  10:             {

  11:                 continue;

  12:             }

Für den gesuchten Namespace definiere ich den Datei-Header mit den benötigten using-Direktiven. Eventuelle Kommentare können hier mit integriert werden. Dazu muss ich den Bereich für ausführbaren Code mit „#>“ unterbrechen und schreiben direkt die entsprechenden Inhalte in das Template.

   1: namespace <#= gesuchterNamespace #>

   2: {

   3:     using System.Diagnostics.CodeAnalysis;

Anschließend setze ich den ausführbaren Codebereich mit „<#+“ fort, indem ich in einer weiteren Schleife nach partiellen Klassen suche und das Template definiere, das für jede gefundene partielle Klasse einzusetzen ist.

   1: foreach(EnvDTE.CodeElement member in codeNamespace.Members)

   2: {

   3:     if(member.Kind != vsCMElement.vsCMElementClass)

   4:     {

   5:         continue;

   6:     }

   7:     // prüfe, ob Klasse partial ist

   8:     CodeClass2 myClass = member as CodeClass2; // benötigt EnvDTE80

   9:     if(myClass == null || myClass.ClassKind != vsCMClassKind.vsCMClassKindPartialClass)

  10:     {

  11:         continue;

  12:     } #>

  13:  

  14:     [ExcludeFromCodeCoverage]

  15:     public partial class <#= member.Name #>

  16:     {

  17:     }

  18:     <#+

  19: }

Das Ergebnis

Mit jedem (Zwischen-)Speichern der Vorlage startet Visual Studio das integrierte „TextTemplatingTool“. Visual Studio zeigt daraufhin die Sicherheitswarnung: Führen Sie keine Vorlagen von nicht vertrauenswürdigen Quellen aus. Nach dem Bestätigen der Meldung wird die C#-Datei anhand der Vorlage als Kind-Element der tt-Datei erstellt.
Vorlage und resultierende C# Datei
Eine erneute Codeanalyse zeigt jetzt eine Abdeckung von knapp 96%.


Fazit

Die Unterstützung durch Visual Studio bietet leider nicht den gewohnten Komfort, was vermutlich der Grund dafür ist, warumT4 viel zu wenig eingesetzt wird. Das Erstellen der Vorlage bedeutet allerdings einen einmaligen Aufwand, der sich lohnt. Das Resultat ist eine strukturierte Vorlage, die einfach zu ändern und noch einfacher auszuführen ist.
Innerhalb der Vorlage kann beliebiger (C#/VB-)Code zum Einsatz kommen, wodurch T4 ein mächtiges Werkzeug für jeden Entwickler darstellt.

Weitere Quellen:

Vollständiger Code:

   1: <#@ template debug="true" hostspecific="true" language="C#" #>

   2: <#@ assembly name="System.Core" #>

   3: <#@ assembly name="System" #>

   4: <#@ assembly name="EnvDTE" #> 

   5: <#@ assembly name="EnvDTE80" #> 

   6: <#@ import namespace="EnvDTE" #>

   7: <#@ import namespace="EnvDTE80" #>

   8: <#@ import namespace="System.CodeDom" #>

   9: <#@ import namespace="System.Linq" #>

  10: <#@ import namespace="System.Text" #>

  11: <#@ import namespace="System.Collections.Generic" #>

  12: <#@ import namespace="System.Reflection" #>

  13: <#@ output extension=".cs" #>

  14:  

  15: <# 

  16:     IServiceProvider hostServiceProvider = (IServiceProvider)Host;

  17:     EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));

  18:     EnvDTE.ProjectItem containingProjectItem = dte.Solution.FindProjectItem(Host.TemplateFile);

  19:     EnvDTE.Project project = containingProjectItem.ContainingProject;

  20:  

  21:     foreach (EnvDTE.ProjectItem projectItem in project.ProjectItems)

  22:     {

  23:         ProcessProjectItem(projectItem);

  24:     }

  25: #>

  26: <#+ 

  27:     // hier steht der gesuchte Namespace

  28:     // ================================

  29:     string ns = "Sdx.....Data.Xrm";

  30:     // ================================

  31:     bool found = false; 

  32:     private void ProcessProjectItem(EnvDTE.ProjectItem projectItem)

  33:     {

  34:         EnvDTE.FileCodeModel fileCodeModel = projectItem.FileCodeModel;    

  35:         if(fileCodeModel != null)

  36:         {

  37:             foreach(EnvDTE.CodeElement codeElement in fileCodeModel.CodeElements)

  38:             {

  39:                 if(found)

  40:                 {

  41:                     break;

  42:                 }

  43:  

  44:                 EnvDTE.CodeNamespace codeNamespace = codeElement as EnvDTE.CodeNamespace;

  45:                 if(codeNamespace == null || codeNamespace.FullName != ns)

  46:                 {

  47:                     continue;

  48:                 }

  49:  

  50:                 found = true;

  51: #>

  52: // --------------------------------------------------------------------------------------------------------------------

  53: // <copyright file="SDXCrmTypesExt.cs" company="SDX-AG">

  54: //   2013

  55: // </copyright>

  56: // <summary>

  57: //   The class part for excluding the class from the code coverage analysis

  58: //   This part is auto generated. All changes done here will be overwritten.

  59: // </summary>

  60: // <auto-generated />

  61: // --------------------------------------------------------------------------------------------------------------------

  62:  

  63: namespace <#= ns #>

  64: {

  65:     using System.Diagnostics.CodeAnalysis;

  66:  <#+            

  67:                 foreach(EnvDTE.CodeElement member in codeNamespace.Members)

  68:                 {

  69:                     // prüfe, ob Element eine Klasse ist

  70:                     if(member.Kind != vsCMElement.vsCMElementClass)

  71:                     {

  72:                         continue;

  73:                     }

  74:                     

  75:                     // prüfe, ob Klasse partial ist

  76:                     CodeClass2 myClass = member as CodeClass2;

  77:                     if(myClass == null || myClass.ClassKind != vsCMClassKind.vsCMClassKindPartialClass)

  78:                     {

  79:                         continue;

  80:                     }

  81:                     #>

  82:     

  83:     /// <summary>

  84:     /// The class part for excluding the class from the code coverage analysis

  85:     /// This part is auto generated. All changes done here will be overwritten.

  86:     /// </summary>

  87:     [ExcludeFromCodeCoverage]

  88:     public partial class <#= member.Name #>

  89:     {

  90:     }

  91: <#+

  92:                 }

  93:                 WriteLine("}");

  94:             }

  95:         }

  96:     }

  97: #>