Agiles C++

Agile C++-Entwicklung und Tests mit Visual Studio und TFS

John Socha-Leialoha

Beispielcode herunterladen.

Sie entwickeln oder testen eine Anwendung, die in Visual C++ erstellt wird. Würden Sie bei der Entwicklung gerne produktiver sein, Code von höherer Qualität produzieren und den Code nach Bedarf zur Verbesserung der Architektur neu schreiben, ohne Fehler befürchten zu müssen? Und würden Sie beim Testen gerne weniger Zeit für das Schreiben und Pflegen von Tests aufwenden, sodass Sie Zeit für andere Testaktivitäten haben?

In diesem Artikel stelle ich eine Reihe von Techniken vor, die unser Team hier bei Microsoft zum Erstellen von Anwendungen verwendet hat.

Das Team ist recht klein. Zehn Mitarbeiter arbeiten gleichzeitig an drei verschiedenen Projekten. Diese Projekte werden in C# und in C++ geschrieben. Der C++-Code wird hauptsächlich für die Programme verwendet, die unter Windows PE ausgeführt werden müssen. Windows PE ist eine reduzierte Windows-Version, die häufig zur Betriebssysteminstallation eingesetzt wird und auch als Teil einer Microsoft System Center Configuration Manager-Aufgabensequenz dient, um Aufgaben auszuführen, die mit dem vollständigen Betriebssystem nicht möglich sind, zum Beispiel das Erfassen einer Festplatte in einer virtuellen Festplattendatei (VHD). Viele Aufgaben für ein kleines Team, das daher produktiv arbeiten muss.

Unser Team verwendet Visual Studio 2010 und Team Foundation Server (TFS) 2010. TFS 2010 dient zur Versionskontrolle, Arbeitsüberwachung, fortlaufenden Integration, zum Sammeln von Codeabdeckungsdaten und zur Berichterstelllung.

Wann und warum unser Team Tests schreibt

Ich beginne mit den Gründen unseres Teams für das Schreiben von Tests. Für Ihr Team kann es andere Gründe geben. Die genaue Antwort fällt für unsere Entwickler und Tester leicht unterschiedlich aus, wenn auch nicht so verschieden, wie man zuerst vielleicht denkt. Dies sind meine Ziele als Entwickler:

  • keine Buildunterbrechungen
  • keine Regressionen
  • sicheres Umgestalten
  • sicheres Ändern der Architektur
  • Fördern des Entwurfs durch testgesteuerte Entwicklung (Test-Driven Development, TDD)

Hinter diesen Zielen steht natürlich der übergeordnete Grund: die Qualität. Wenn diese Ziele erreicht werden, können die Entwickler viel produktiver sein und viel mehr Freude an der Arbeit haben.

Hinsichtlich der Ziele unserer Tester gehe ich nur auf einen Aspekt eines Agile-Testers ein: das Schreiben von automatisierten Tests. Die Vermeidung von Regressionen, die akzeptanzgesteuerte Entwicklung sowie das Sammeln von Codeabdeckungsdaten und die Berichterstellung darüber gehören zu den Zielen der Tester beim Schreiben von automatisierten Tests.

Unsere Tester tun selbstverständlich viel mehr, als nur automatisierte Tests zu schreiben. Sie sind für das Sammeln von Codeabdeckungsdaten verantwortlich, da die Codeabdeckungszahlen die Ergebnisse von allen Tests anstatt nur von Komponententests enthalten sollen. Mehr zu diesem Thema später.

Ich beschreibe in diesem Artikel die verschiedenen Tools und Techniken, die unser Team verwendet, um die hier genannten Ziele zu erreichen.

Beseitigen von Buildunterbrechungen durch abgegrenzte Eincheckvorgänge

Das Team nutzte früher Verzweigungen, damit die Tester immer einen stabilen Build zum Testen hatten. Mit der Pflege der Verzweigungen ist allerdings ein Aufwand verbunden. Da wir jetzt abgegrenzte Eincheckvorgänge nutzen, verwenden wir die Verzweigungen nur noch für Veröffentlichungen, eine willkommene Veränderung.

Für abgegrenzte Eincheckvorgänge ist es erforderlich, dass Sie einen Buildcontroller und mindestens einen Build-Agenten eingerichtet haben. Ich gehe hier nicht auf das Thema ein, aber Sie erhalten in der MSDN-Bibliothek auf der Seite „Verwalten des Team Foundation Builds“ unter bit.ly/jzA8Ff weitere Informationen.

Nachdem Sie Build-Agenten eingerichtet und gestartet haben, können Sie eine neue Builddefinition für abgegrenzte Eincheckvorgänge erstellen, indem Sie diese Schritte in Visual Studio ausführen:

  1. Klicken Sie in der Menüleiste auf „Ansicht“ und „Team Explorer“, damit das Team Explorer-Toolfenster angezeigt wird.

  2. Erweitern Sie Ihr Teamprojekt, und klicken Sie mit der rechten Maustaste auf „Erstellen“.

  3. Klicken Sie auf „Neue Builddefinition“.

  4. Klicken Sie links auf „Trigger“, und wählen Sie „Abgegrenzter Eincheckvorgang“ aus, wie in Abbildung 1 dargestellt.

    Select the Gated Check-in Option for Your New Build Definition

    Abbildung 1 Auswählen des abgegrenzten Eincheckvorgangs für die neue Builddefinition

  5. Klicken Sie auf „Build-Standardwerte“, und wählen Sie den Buildcontroller aus.

  6. Klicken Sie auf „Prozess“, und wählen Sie die Elemente zur Erstellung aus.

Sobald Sie diese Builddefinition gespeichert haben (wir haben unsere „Gated Checkin“ genannt), wird ein neues Dialogfeld angezeigt, nachdem Sie Ihren Eincheckvorgang übermittelt haben (siehe Abbildung 2). Durch Klicken auf „Änderungen erstellen“ wird ein Shelveset erstellt und an den Buildserver gesendet. Wenn keine Buildfehler auftreten und alle Komponententests bestanden wurden, werden Ihre Änderungen von TFS eingecheckt. Andernfalls wird der Eincheckvorgang von TFS abgelehnt.

Gated Check-in Dialog Box

Abbildung 2 Dialogfeld „Abgegrenzter Eincheckvorgang“

Abgegrenzte Eincheckvorgänge sind sehr praktisch. Sie stellen sicher, dass keine Buildunterbrechungen auftreten und alle Komponententests erfolgreich verlaufen. Als Entwickler kann man nur zu leicht vergessen, vor dem Eincheckvorgang alle Tests auszuführen. Aber mit abgegrenzten Eincheckvorgängen gehört dies der Vergangenheit an.

Schreiben von C++-Komponententests

Da Sie jetzt wissen, wie Sie die Komponententests als Teil eines abgegrenzten Eincheckvorgangs ausführen, gehe ich auf eine Methode ein, mit der Sie diese Komponententests für systemeigenen C++-Code schreiben können.

Ich bin aus mehreren Gründen ein großer Anhänger von TDD. Mit TDD kann ich mich besser auf das Verhalten konzentrieren, wodurch meine Entwürfe einfacher bleiben. Ich habe außerdem ein Sicherheitsnetz durch Tests, die den Verhaltensvertrag definieren. Ich kann umgestalten, ohne zu befürchten, dass ich Fehler als Folge von unbeabsichtigten Verletzungen des Verhaltensvertrags hinzufüge. Und ich weiß, dass andere Entwickler kein notwendiges Verhalten beschädigen, von dem sie keine Kenntnis hatten.

Einer der Entwickler im Team verwendete zum Testen von C++-Code das integrierte Testprogramm (mstest). Er schrieb Microsoft .NET Framework-Komponententests mit C++/CLI. Der Code rief öffentliche Funktionen auf, welche durch eine systemeigene C++-DLL verfügbar gemacht wurden. In diesem Abschnitt zeige ich eine Weiterführung dieses Ansatzes, sodass Sie native C++-Klassen, die sich im Produktionscode befinden, direkt instanziieren können. Anders gesagt, können Sie mehr als nur die öffentliche Schnittstelle testen.

Die Lösung besteht darin, den Produktionscode in eine statische Bibliothek einzufügen, die sowohl mit den Komponententest-DLLs als auch mit der Produktions-EXE oder -DLL verknüpft werden kann, wie in Abbildung 3 gezeigt.

Abbildung 3 Gemeinsamer Code für die Tests und das Produkt über eine statische Bibliothek

Hier folgen die Schritte, mit denen Sie die Projekte für dieses Verfahren einrichten. Erstellen Sie zuerst die statische Bibliothek:

  1. Klicken Sie in Visual Studio auf „Datei“, „Neu“ und „Projekt“.
  2. Klicken Sie in der Liste der installierten Vorlagen auf „Visual C++“. Sie müssen dazu „Andere Sprachen“ erweitern.
  3. Klicken Sie in der Liste mit Projekttypen auf „Win32-Projekt“.
  4. Geben Sie den Projektnamen ein, und klicken Sie auf „OK“.
  5. Klicken Sie auf „Weiter“, auf „Statische Bibliothek“ und auf „Fertig stellen“.

Erstellen Sie jetzt die Testumgebung. Zum Einrichten eines Testprojekts sind einige weitere Schritte erforderlich. Sie müssen das Projekt erstellen, ihm aber auch Zugriff auf den Code und die Headerdateien in der statischen Bibliothek geben.

Klicken Sie zunächst im Fenster „Projektmappen-Explorer“ auf die Lösung. Klicken Sie auf „Hinzufügen“ und anschließend auf „Neues Projekt“. Klicken Sie in der Vorlagenliste unter dem Visual C++-Knoten auf „Test“. Geben Sie den Projektnamen ein (unser Team fügt „UnitTests“ am Ende des Projektnamens hinzu), und klicken Sie auf „OK“.

Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das neue Projekt und dann auf „Eigenschaften“. Klicken Sie in der Struktur links auf „Allgemeine Eigenschaften“. Klicken Sie auf „Neuen Verweis hinzufügen“. Klicken Sie auf die Registerkarte „Projekte“, wählen Sie das Projekt mit Ihrer statischen Bibliothek aus, und klicken Sie auf „OK“, um den Dialog zum Hinzufügen eines Verweises zu schließen.

Erweitern Sie in der Struktur links den Knoten „Konfigurationseigenschaften“ und anschließend den C/C++-Knoten. Klicken Sie unter dem C/C++-Knoten auf „Allgemein“. Klicken Sie auf das Kombinationsfeld „Konfiguration“, und wählen Sie „Alle Konfigurationen“ aus, damit Sie sowohl Debug- als auch Releaseversionen ändern.

Klicken Sie auf die Option zum Hinzufügen von Bibliotheken, und geben Sie den Pfad zur gewünschten statischen Bibliothek an, wobei Sie anstelle von „MyStaticLib“ den Namen Ihrer statischen Bibliothek einfügen:

$(SolutionDir)\MyStaticLib;%(AdditionalIncludeDirectories)

Klicken Sie auf die Eigenschaft „Common Language Runtime-Unterstützung“ in der gleichen Eigenschaftenliste, und ändern Sie sie in „Common Language Runtime-Unterstützung (/clr)“.

Klicken Sie unter „Konfigurationseigenschaften“ auf den Abschnitt „Allgemein“, und ändern Sie die Eigenschaft „TargetName“ in „$(ProjectName)“. Hier ist standardmäßig für alle Testprojekte „DefaultTest“ angegeben, aber es sollte der Name des Projekts sein. Klicken Sie auf „OK“.

Wiederholen Sie den ersten Teil dieses Verfahrens, um die statische Bibliothek zur Produktions-EXE oder -DLL hinzuzufügen.

Schreiben des ersten Komponententests

Sie haben jetzt alles, was Sie zum Schreiben eines neuen Komponententests benötigen. Als Testmethoden dienen in C++ geschriebene .NET-Methoden, sodass die Syntax ein wenig von systemeigener C++-Syntax abweicht. Wenn Sie mit C# vertraut sind, erkennen Sie in mancherlei Hinsicht eine Mischung zwischen C++- und C#-Syntax. Weitere Informationen erhalten Sie in der MSDN-Bibliotheksdokumentation „Sprachfeatures zum Verweisen auf die CLR“ unter bit.ly/iOKbR0.

Nehmen wir an, Sie möchten eine Klassendefinition testen, die in etwa so aussieht:

#pragma once
class MyClass {
  public:
    MyClass(void);
    ~MyClass(void);

    int SomeValue(int input);
};

Sie möchten jetzt einen Test für die SomeValue-Methode schreiben, um das Verhalten für die Methode festzulegen. In Abbildung 4 wird dargestellt, wie ein einfacher Komponententest aussehen kann. Die vollständige CPP-Datei wird angezeigt.

Abbildung 4 Einfacher Komponententest

#include "stdafx.h"
#include "MyClass.h"
#include <memory>
using namespace System;
using namespace Microsoft::VisualStudio::TestTools::UnitTesting;

namespace MyCodeTests {
  [TestClass]
  public ref class MyClassFixture {
    public:
      [TestMethod]
      void ShouldReturnOne_WhenSomeValue_GivenZero() {
        // Arrange
        std::unique_ptr<MyClass> pSomething(new MyClass);
 
        // Act
        int actual = pSomething->SomeValue(0);
 
        // Assert
        Assert::AreEqual<int>(1, actual);
      }
  };
}

Falls Sie nicht mit dem Schreiben von Komponententests vertraut sind, ich verwende ein Muster, das als „Arrange, Act, Assert“ bekannt ist. Im Abschnitt „Arrange“ werden die Vorbedingungen für das Szenario eingerichtet, das Sie testen möchten. Unter „Act“ wird die Methode aufgerufen, die Sie testen. Unter „Assert“ überprüfen Sie, dass die Methode das gewünschte Verhalten gezeigt hat. Zur besseren Lesbarkeit füge ich vor jedem Abschnitt gerne einen Kommentar ein und kann so schnell den Abschnitt „Act“ finden.

Wie Sie in Abbildung 4 sehen, sind Testmethoden durch das TestMethod-Attribut gekennzeichnet. Diese Methoden müssen wiederum in einer Klasse enthalten sein, die durch das TestClass-Attribut gekennzeichnet ist.

Die erste Codezeile in der Testmethode erstellt eine neue Instanz der systemeigenen C++-Klasse. Ich verwende gern die unique_ptr-C++-Standardbibliotheksklasse, um sicherzustellen, dass diese Instanz am Ende der Testmethode automatisch gelöscht wird. Sie können daher deutlich sehen, dass Sie systemeigenen C++-Code mit Ihrem CLI/C++-.NET-Code mischen können. Es gibt natürlich Einschränkungen, die ich im nächsten Abschnitt erläutere.

Noch einmal, wenn Sie bisher keine .NET-Tests geschrieben haben, die Assert-Klasse hat eine Anzahl von nützlichen Methoden, mit denen Sie verschiedene Bedingungen überprüfen können. Ich verwende bevorzugt die generische Version, um den vom Ergebnis erwarteten Datentyp deutlich zu machen.

C++/CLI-Tests in vollem Umfang nutzen

Wie bereits erwähnt, müssen Sie einige Einschränkungen kennen, wenn Sie systemeigenen C++-Code mit C++/CLI-Code mischen. Die Unterschiede sind eine Folge der unterschiedlichen Speicherverwaltung der zwei Codebasen. Systemeigenes C++ verwendet den new-Operator von C++, um Speicher zuzuweisen, und Sie sind selbst dafür verantwortlich, diesen Speicher freizugeben. Sobald Sie einen Speicherbereich zuordnen, befinden sich die Daten immer am selben Platz.

Andererseits haben Zeiger in C++/CLI-Code aufgrund des vom .NET Framework geerbten Garbage Collection-Modells ein sehr unterschiedliches Verhalten. Sie erstellen neue .NET-Objekte in C++/CLI, indem Sie den gcnew-Operator anstelle des new-Operators verwenden, wodurch ein Objekthandle anstelle eines Zeigers auf das Objekt zurückgegeben wird. Handles sind im Grunde Zeiger auf einen Zeiger. Wenn die Garbage Collection verwaltete Objekte im Speicher verschiebt, werden die Handles mit dem neuen Speicherort aktualisiert.

Sie müssen beim Mischen verwalteter und systemeigener Zeiger sehr sorgfältig vorgehen. Ich gehe auf einige dieser Unterschiede ein und stelle Ihnen Tipps und Tricks vor, wie Sie C++/CLI-Tests optimal für systemeigene C++-Objekte nutzen.

Nehmen wir an, dass Sie eine Methode testen möchten, die einen Zeiger an eine Zeichenfolge zurückgibt. In C++ können Sie den Zeichenfolgenzeiger mit LPCTSTR darstellen. Aber eine .NET-Zeichenfolge wird in C++/CLI durch „String^“ repräsentiert. Die Einfügemarke nach dem Klassennamen stellt ein Handle für ein verwaltetes Objekt dar.

Hier ist ein Beispiel dafür, wie Sie den Wert einer Zeichenfolge testen, die von einem Methodenaufruf zurückgegeben wird:

// Act
LPCTSTR actual = pSomething->GetString(1);
 
// Assert
Assert::AreEqual<String^>("Test", gcnew String(actual));

Die letzte Zeile enthält alle Details. Es gibt eine AreEqual-Methode, die verwaltete Zeichenfolgen akzeptiert, aber es gibt keine entsprechende Methode für systemeigene C++-Zeichenfolgen. Daher müssen Sie verwaltete Zeichenfolgen verwenden. Der erste Parameter für die AreEqual-Methode ist eine verwaltete Zeichenfolge. Tatsächlich ist es eine Unicode-Zeichenfolge, obwohl es nicht als solche gekennzeichnet ist, zum Beispiel durch Verwendung von „_T“ oder „L“.

Die String-Klasse hat einen Konstruktor, der eine C++-Zeichenfolge akzeptiert. Sie können also eine neue verwaltete Zeichenfolge erstellen, die den tatsächlichen Wert aus der Methode enthält, die Sie testen. An diesem Punkt stellt „AreEqual“ sicher, dass die Werte übereinstimmen.

Die Assert-Klasse hat zwei anscheinend sehr erfolgversprechende Methoden: „IsNull“ und „IsNotNull“. Der Parameter für diese Methode ist aber ein Handle, kein Objektzeiger, wodurch die Methoden nur mit verwalteten Objekten verwendbar sind. Verwenden Sie stattdessen wie in folgendem Beispiel die IsTrue-Methode:

Assert::IsTrue(pSomething != nullptr, "Should not be null");

Dadurch wird dasselbe erreicht, nur mit etwas mehr Code. Ich füge einen Kommentar hinzu, damit in der Meldung, die im Testergebnisfenster (siehe Abbildung 5) angezeigt wird, die Erwartung klar ist.

Test Results Showing the Additional Comment in the Error Message

Abbildung 5 Testergebnisse mit dem zusätzlichen Kommentar in der Fehlermeldung

Gemeinsamer Setup- und Beendigungscode

Behandeln Sie den Testcode wie Produktionscode. Anders ausgedrückt, Tests sollten genauso viel umgestaltet werden wie Produktionscode, damit der Testcode leicht pflegbar bleibt. An einem bestimmten Punkt haben Sie möglicherweise einigen gemeinsamen Setup- und Beendigungscode für die gesamten Testmethoden in einer Testklasse. Sie können eine Methode bestimmen, die vor jedem Test ausgeführt wird, und eine weitere, die nach jedem Test ausgeführt wird. Nur eine davon, beide oder keine sind möglich.

Das TestInitialize-Attribut kennzeichnet eine Methode, die vor jeder Testmethode in der Testklasse ausgeführt wird. Dementsprechend kennzeichnet TestCleanup eine Methode, die nach jeder Testmethode in der Testklasse ausgeführt wird. Im Folgenden finden Sie ein Beispiel:

[TestInitialize]
void Initialize() {
  m_pInstance = new MyClass;
}
 
[TestCleanup]
void Cleanup() {
  delete m_pInstance;
}

MyClass *m_pInstance;

Ich habe zuerst einen einfachen Zeiger auf die Klasse für „m_pInstance“ verwendet. Aus welchem Grund verwende ich nicht „unique_ptr“, um die Lebensdauer zu verwalten?

Die Antwort hängt wieder mit der Mischung von systemeigenem C++-Code und C++/CLI-Code zusammen. Instanzvariablen in C++/CLI sind Teil eines verwalteten Objekts und können daher nur Handles für verwaltete Objekte, Zeiger auf systemeigene Objekte oder Werttypen sein. Zum Verwalten der Lebensdauer der systemeigenen C++-Instanzen müssen Sie mit „new“ und „delete“ auf die Grundlagen zurückgreifen.

Verwenden von Zeigern auf Instanzvariablen

Wenn Sie COM verwenden, kommen Sie vielleicht in die Lage, dass Sie ähnlichen Code wie den folgenden schreiben möchten:

[TestMethod]
Void Test() {
  ...
  HRESULT hr = pSomething->GetWidget(&m_pUnk);
  ...
}

IUnknown *m_pUnk;

Dieser Code wird nicht kompiliert und erzeugt ungefähr folgende Fehlermeldung:

cannot convert parameter 1 from 'cli::interior_ptr<Type>' to 'IUnknown **'

Eine C++/CLI-Instanzvariable hat in diesem Fall den Typ „interior_ptr<IUnknown *>“, welcher nicht mit systemeigenem C++-Code kompatibel ist. Sie fragen sich nach dem Grund hierfür? Mein Ziel war nur ein Zeiger.

Die Testklasse ist eine verwaltete Klasse, deren Instanzen daher im Speicher vom Garbage Collector verschoben werden können. Wenn also ein Zeiger auf eine Instanzvariable zeigt und das Objekt anschließend verschoben wird, führt dies zur Ungültigkeit des Zeigers.

Sie können das Objekt für die Dauer des systemeigenen Aufrufs wie folgt sperren:

cli::pin_ptr<IUnknown *> ppUnk = &m_pUnk;
HRESULT hr = pSomething->GetWidget(ppUnk);

Die erste Zeile sperrt die Instanz so lange, bis die Variable den Bereich verlässt. Dadurch können Sie einen Zeiger an die Instanzvariable zu systemeigenem C++ übergeben, obwohl diese Variable in einer verwalteten Testklasse enthalten ist.

Schreiben von testbarem Code

Zu Beginn dieses Artikels habe ich erwähnt, wie wichtig das Schreiben von testbarem Code ist. Ich verwende TDD, um sicherzustellen, dass mein Code testbar ist. Einige Entwickler schreiben Tests aber lieber gleich nach dem Code. In jedem Fall ist es wesentlich, nicht nur an die Komponententests, sondern an die gesamten Tests zu denken.

Der bekannte und produktive Agile-Autor Mike Cohn hat eine Testautomatisierungspyramide entworfen, die eine Vorstellung davon vermittelt, welche Testtypen und wie viele Tests auf den einzelnen Ebenen erforderlich sind. Die Entwickler müssen alle oder die meisten Komponententests und möglicherweise einige Integrationstests schreiben. Weitere Informationen zur Testpyramide erhalten Sie in Mike Cohns Blogbeitrag „Die vergessene Ebene der Testautomatisierungspyramide“ unter bit.ly/eRZU2p.

Die Tester sind normalerweise dafür verantwortlich, Akzeptanz- und UI-Tests zu schreiben. Diese werden manchmal auch End-to-End- oder E2E-Tests genannt. In Mike Cohns Pyramide ist das UI-Dreieck kleiner als die Bereiche der anderen Testtypen. Dahinter steht das Konzept, so wenig automatisierte UI-Tests wie möglich zu schreiben. Automatisierte UI-Tests sind eher empfindlich, und es ist teuer, sie zu schreiben und zu pflegen. Kleine Änderungen an der UI können die UI-Tests leicht beschädigen.

Wenn der Code nicht testbar geschrieben ist, kann sich die Pyramide ohne Weiteres umkehren. In diesem Fall sind die meisten der automatisierten Tests UI-Tests, was eine unangenehme Situation ist. Aber das Fazit ist, dass die Entwickler dafür Sorge tragen müssen, dass die Tester Integrations- und Akzeptanztests unter der UI schreiben können.

Aus unerklärlichen Gründen schreiben außerdem die meisten der mir bekannten Tester gerne Tests in C#, scheuen aber vor dem Schreiben in C++ zurück. Folglich benötigte unser Team eine Brücke zwischen dem zu testenden C++-Code und den automatisierten Tests. Diese Brücke besteht aus Fixtures, wobei es sich um C++/CLI-Klassen handelt, die für den C#-Code aussehen wie eine beliebige andere verwaltete Klasse.

Erstellen von C# zu C++-Fixtures

Die Techniken hier unterscheiden sich nur wenig von denen, auf die ich für das Schreiben von C++/CLI-Tests eingegangen bin. Beide verwenden den gleichen Typ gemischten Code. Der Unterschied liegt darin, wie sie am Ende verwendet werden.

Als ersten Schritt erstellen Sie ein neues Projekt, das die Fixtures enthalten soll:

  1. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Lösungsknoten, klicken Sie auf „Hinzufügen“ und dann auf „Neues Projekt“.
  2. Klicken Sie unter „Andere Sprachen“ unter „Visual C++/CLR“ auf „Klassenbibliothek“.
  3. Geben Sie den gewünschten Projektnamen ein, und klicken Sie auf „OK“.
  4. Wiederholen Sie die Schritte, um ein Testprojekt zum Hinzufügen eines Verweises und der Includedateien zu erstellen.

Die Fixture-Klasse sieht der Testklasse ähnlich, allerdings ohne die verschiedenen Attribute (siehe Abbildung 6).

Abbildung 6 C# zu C++-Testfixture

#include "stdafx.h"
#include "MyClass.h"
using namespace System;
 
namespace MyCodeFixtures {
  public ref class MyCodeFixture {
    public:
      MyCodeFixture() {
        m_pInstance = new MyClass;
      }
 
      ~MyCodeFixture() {
        delete m_pInstance;
      }
 
      !MyCodeFixture() {
        delete m_pInstance;
      }
 
      int DoSomething(int val) {
        return m_pInstance->SomeValue(val);
      }
 
      MyClass *m_pInstance;
  };
}

Es gibt keine Headerdatei! Das ist eines meiner Lieblingsfeatures in C++/CLI. Da die Klassenbibliothek eine verwaltete Assembly erstellt, werden die Informationen über Klassen als .NET-Typinformationen gespeichert, sodass keine Headerdateien erforderlich sind.

Diese Klasse enthält auch einen Destruktor sowie einen Finalizer, wobei der Destruktor hier tatsächlich nicht der Destruktor ist. Stattdessen schreibt der Compiler den Destruktor neu in eine Implementierung der Dispose-Methode in der IDisposable-Schnittstelle. Daher implementiert jede C++/CLI-Klasse, die einen Destruktor aufweist, die IDisposable-Schnittstelle.

Die !MyCodeFixture-Methode ist der Finalizer, der vom Garbage Collector aufgerufen wird, wenn dieser sein Objekt freigibt, sofern Sie nicht vorher die Dispose-Methode aufgerufen haben. Sie können entweder die using-Anweisung verwenden, um die Lebensdauer des eingebetteten systemeigenen C++-Objekts zu steuern, oder die Lebensdauer dem Garbage Collector überlassen. Weitere Informationen zu diesem Verhalten finden Sie im MSDN-Bibliotheksartikel „Änderungen in der Destruktorsemantik“ unter bit.ly/kW8knr.

Sobald Sie über eine C++/CLI-Fixtureklasse verfügen, können Sie einen C#-Komponententest schreiben, der ungefähr dem in Abbildung 7 entspricht.

Abbildung 7 Ein C#-Komponententestsystem

using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyCodeFixtures;
 
namespace MyCodeTests2 {
  [TestClass]
  public class UnitTest1 {
    [TestMethod]
    public void TestMethod1() {
      // Arrange
      using (MyCodeFixture fixture = new MyCodeFixture()) {
        // Act
        int result = fixture.DoSomething(1);
 
        // Assert
        Assert.AreEqual<int>(1, result);
      }
    }
  }
}

Ich steuere die Lebensdauer des Fixture-Objekts lieber ausdrücklich durch eine using-Anweisung, anstatt mich auf den Garbage Collector zu verlassen. Das ist in Testmethoden besonders wichtig, um sicherzustellen, dass Tests nicht mit anderen Test interagieren.

Erfassen von Codeabdeckung und Erstellen von Berichten zur Codeabdeckung

Als Letztes habe ich zu Beginn des Artikels die Codeabdeckung angesprochen. Mein Team möchte, dass die Codeabdeckung automatisch durch den Buildserver erfasst und an TFS veröffentlicht wird, wodurch sie für alle leicht zugänglich ist.

Zuerst musste ich herausfinden, wie C++-Codeabdeckung aus ausgeführten Tests erfasst wird. Ich fand im Web einen aufschlussreichen Blogbeitrag von Emil Gustafsson mit dem Titel „Berichte zur systemeigenen C++-Codeabdeckung mit Visual Studio 2008 Team System“ (bit.ly/eJ5cqv). In diesem Beitrag sind die Schritte enthalten, die zum Erfassen der Codeabdeckungsdaten erforderlich sind. Ich erstellte daraus eine CMD-Datei, die ich auf meinem Entwicklungscomputer jederzeit ausführen kann, um Codeabdeckungsdaten zu erfassen:

"%VSINSTALLDIR%\Team Tools\Performance Tools\vsinstr.exe" Tests.dll /COVERAGE
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsperfcmd.exe" /START:COVERAGE /WaitStart /OUTPUT:coverage
mstest /testcontainer:Tests.dll /resultsfile:Results.trx
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsperfcmd.exe" /SHUTDOWN

Ersetzen Sie Tests.dll durch den richtigen Namen der DLL, die Tests enthält. Sie müssen außerdem die DLLs für die Instrumentierung vorbereiten:

  1. Klicken Sie im Fenster „Projektmappen-Explorer“ mit der rechten Maustaste auf das Testprojekt.
  2. Klicken Sie auf „Eigenschaften“.
  3. Wählen Sie die Debugkonfiguration aus.
  4. Erweitern Sie „Konfigurationseigenschaften“, erweitern Sie anschließend „Linker“, und klicken Sie dann auf „Erweitert“.
  5. Ändern Sie die Profile-Eigenschaft in „Yes (/PROFILE)“.
  6. Klicken Sie auf „OK“.

Diese Schritte aktivieren die Profilerstellung, die für die Instrumentierung der Assemblys erforderlich ist, damit Codeabdeckungsdaten erfasst werden können.

Erstellen Sie das Projekt erneut, und führen Sie die CMD-Datei aus. Dadurch wird eine Abdeckungsdatei erstellt. Laden Sie diese in Visual Studio, damit Sie die Codeabdeckungsdaten aus den Tests erfassen können.

Die Ausführung dieser Schritte auf dem Buildserver und die Veröffentlichung der Ergebnisse in TFS erfordern eine benutzerdefinierte Buildvorlage. TFS-Buildvorlagen sind in der Versionskontrolle gespeichert und gehören zu einem bestimmten Teamprojekt. Sie finden unter jedem Teamprojekt einen Ordner namens „BuildProcessTemplates“, der höchstwahrscheinlich mehrere Buildvorlagen enthält.

Um die im Download enthaltene Buildvorlage zu verwenden, öffnen Sie das Fenster „Quellcodeverwaltungs-Explorer“. Navigieren Sie im Teamprojekt zum Ordner „BuildProcessTemplates“, und stellen Sie sicher, dass er einem Verzeichnis auf dem Computer zugeordnet ist. Kopieren Sie die Datei „BuildCCTemplate.xaml“ an diesen zugeordneten Speicherort. Fügen Sie diese Vorlage der Quellcodeverwaltung hinzu, und checken Sie sie ein.

Vorlagendateien müssen eingecheckt werden, bevor Sie sie in Builddefinitionen verwenden können.

Nachdem Sie nun die Buildvorlage eingecheckt haben, können Sie eine Builddefinition zum Ausführen der Codeabdeckung erstellen. Wie schon gezeigt, werden C++-Codeabdeckungsdaten mithilfe des Befehls „vsperfmd“ gesammelt. Während seiner Ausführung überwacht „vsperfmd“ die Codeabdeckungsdaten für alle instrumentierten ausführbaren Dateien, die in dieser Zeit ausgeführt werden. Vermeiden Sie deshalb, dass zur selben Zeit andere instrumentierte Tests ausgeführt werden. Stellen Sie außerdem sicher, dass auf dem Computer, der diese Codeabdeckungsläufe verarbeitet, nur ein Build-Agent ausgeführt wird.

Ich habe eine Builddefinition erstellt, die nachts ausgeführt wird. Führen Sie zum Erstellen einer solchen Version die folgenden Schritte aus:

  1. Erweitern Sie im Team Explorer-Fenster den Knoten für Ihr Teamprojekt.
  2. Klicken Sie mit der rechten Maustaste unter dem Teamprojekt auf den Knoten „Builds“.
  3. Klicken Sie auf „Neue Builddefinition“.
  4. Klicken Sie im Abschnitt „Trigger“ auf „Plan“, und wählen Sie die Tage aus, an denen die Codeabdeckung ausgeführt werden soll.
  5. Klicken Sie im Abschnitt „Prozess“ oben im Abschnitt „Buildprozessvorlage“ auf „Details anzeigen“. Wählen Sie dann die Buildvorlage aus, die Sie in die Quellcodeverwaltung eingecheckt haben.
  6. Füllen Sie die anderen erforderlichen Abschnitte aus, und speichern Sie.

Hinzufügen einer Testeinstellungsdatei

Für die Builddefinition ist auch eine Testeinstellungsdatei erforderlich. Dies ist eine XML-Datei, in der die DLLs aufgelistet sind, für die Sie die Ergebnisse erfassen und veröffentlichen möchten. Mit den folgenden Schritten richten Sie diese Datei für die Codeabdeckung ein:

  1. Doppelklicken Sie zum Öffnen des Dialogfelds „Testeinstellungen“ auf die Einstellungsdatei „Local.test“.
  2. Klicken Sie in der Liste links auf „Daten und Diagnose“.
  3. Klicken Sie auf „Codeabdeckung“, und aktivieren Sie das Kontrollkästchen.
  4. Klicken Sie über der Liste auf die Schaltfläche „Konfigurieren“.
  5. Aktivieren Sie das Kontrollkästchen neben der DLL, die Ihre Tests enthält (einschließlich des Codes, der durch die Tests getestet wird).
  6. Deaktivieren Sie „Assemblys direkt instrumentieren“, da dies durch die Builddefinition durchgeführt wird.
  7. Klicken Sie auf „OK“, „Übernehmen“ und dann auf „Schließen“.

Wenn Sie mehr als eine Lösung erstellen möchten oder mehr als ein Testprojekt haben, benötigen Sie eine Kopie der Testeinstellungsdatei, welche die Namen aller Assemblys enthält, die für die Codeabdeckung überwacht werden sollen.

Kopieren Sie dazu die Testeinstellungsdatei in den Stamm der Verzweigung und geben Sie ihr einen beschreibenden Namen, z. B. „CC.testsettings“. Bearbeiten Sie die XML-Datei. Sie enthält mindestens ein CodeCoverageItem-Element aus den vorherigen Schritten. Für jede DLL, die erfasst werden soll, fügen Sie einen Eintrag hinzu. Beachten Sie dabei, dass die Pfade relativ zum Speicherort der Projektdatei und nicht zu dem der Testeinstellungsdatei sind. Checken Sie diese Datei in die Quellcodeverwaltung ein.

Zum Schluss müssen Sie die Builddefinition ändern, damit sie diese Testeinstellungsdatei verwendet:

  1. Erweitern Sie im Team Explorer-Fenster den Knoten für Ihr Teamprojekt und dann „Builds“.
  2. Klicken Sie mit der rechten Maustaste auf die Builddefinition, die Sie vorher erstellt haben.
  3. Klicken Sie auf „Builddefinition bearbeiten“.
  4. Erweitern Sie im Abschnitt „Prozess“ die Option „Automatisierte Tests“ und dann „1. Testassembly“. Klicken Sie auf „Testeinstellungsdatei“. Klicken Sie auf die Schaltfläche „...“, und wählen Sie die zuvor erstellte Testeinstellungsdatei aus.
  5. Speichern Sie die Änderungen.

Sie können diese Builddefinition testen, indem Sie einen Rechtsklick darauf ausführen und „Neuen Build in die Warteschlange stellen“ auswählen, um direkt einen neuen Build zu starten.

Erstellen von Berichten zur Codeabdeckung

Ich habe einen benutzerdefinierten SQL Server Reporting Services-Bericht erstellt, in dem die Codeabdeckung gezeigt wird, siehe Abbildung 8. (Die Projektnamen sind nicht lesbar.) Dieser Bericht verwendet eine SQL-Abfrage, um die Daten im TFS-Warehouse zu lesen und die kombinierten Ergebnisse für C++- und C#-Code darzustellen.

The Code-Coverage Report

Abbildung 8 Codeabdeckungsbericht

Ich gehe nicht auf alle Details zur Funktionsweise dieses Berichts ein, möchte aber einige Aspekte erwähnen. Die Datenbank enthält aus zwei Gründen zu viele Informationen aus der C++-Codeabdeckung: Die Ergebnisse enthalten Testmethodencode und Standard-C++-Bibliotheken (die sich in den Headerdateien befinden).

Ich habe der SQL-Abfrage Code hinzugefügt, der diese zusätzlichen Daten herausfiltert. Im Bericht können Sie folgende SQL-Zeilen sehen:

    and CodeElementName not like 'std::%'
    and CodeElementName not like 'stdext::%'
    and CodeElementName not like '`anonymous namespace'':%'
    and CodeElementName not like '_bstr_t%'
    and CodeElementName not like '_com_error%'
    and CodeElementName not like '%Tests::%'

Diese Zeilen schließen Codeabdeckungsergebnisse für bestimmte Namespaces („std“, „stdext“ und „anonymous“) und einige mit Visual C++ gelieferte Klassen („_bstr_t“ und „_com_error“) sowie jeden Code aus, der sich in einem auf „Tests“ endenden Namespace befindet.

Letzteres, der Ausschluss von Namespaces, die auf „Test“ enden, schließt alle Methoden in Testklassen aus. Wenn Sie ein neues Testprojekt erstellen, sind aufgrund der Tatsache, dass der Projektname mit „Tests“ endet, alle Testklassen standardmäßig innerhalb von einem Namespace, der auf „Tests“ endet. Sie können hier weitere Klassen oder Namespaces hinzufügen, die Sie ausschließen möchten.

Ich habe viele Möglichkeiten nur angeschnitten. Informieren Sie sich weiter in meinen Blog unter blogs.msdn.com/b/jsocha.           

John Socha-Leialoha ist Entwickler in der Management Platforms & Service Delivery-Gruppe bei Microsoft. Er hat unter anderem den Norton Commander geschrieben (in C und Assembler) und „Peter Norton’s Assembly Language Book“ (Brady, 1987) verfasst.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Rong Lu