Automatischer SW-Test: Typische Probleme und einfache Lösungsansätze

Einleitung und Motivation

Die im nachfolgenden Text beschriebenen Vorgehensweisen und Realisierungsansätze stammen aus Projekten im Umfeld der Automatisierungstechnik, die unter aktuellen Microsoft-Windows-Betriebssystemen realisiert wurden. Stichpunkte: Parallelisierung von Abläufen, Multithreading, C++, Microsoft Visual Studio.

Die Erstellung und laufende Wartung automatisierter SW-Tests wurde dabei als notwendiges Hilfsmittel zur Gewährleistung einer überzeugenden SW-Qualität betrachtet. Um den Wartungsaufwand in Grenzen zu halten, war dabei auch der Aspekt der Wiederverwendung von Bedeutung.

Querverweise:

Top1 Organisation des Testaufbaus

Top1.1 Simulation der Umgebung

Das Testobjekt wird typischerweise in eine spezifische Testumgebung eingebettet, in der alle benötigten Kommunikationspartner instanziiert werden, so dass aus Sicht des Testobjektes kein Unterschied zum Betrieb in der Produktivumgebung zu erkennen ist.

Die Kommunikationspartner werden dabei häufig in vereinfachter Form simuliert. Teilweise ist jedoch auch das direkte Einbinden von Original-Klassen/Komponenten aus dem Produktivcode möglich und sinnvoll.

Top1.1.1 Typische Struktur

Schematische Darstellung:
Simulation der Umgebung

Bei der Erstellung einer spezifischen Testumgebung sollten folgende Aspekte bedacht werden:

  • Vermeidung zusätzlicher Abhängigkeiten
    Bei der Einbindung vorhandener Originalkomponenten erspart man sich zwar den Realisierungsaufwand für eine entsprechende Simulationskomponente, dieser Vorteil kann jedoch schnell durch folgende Aspekte zunichte gemacht werden:
    • Die einzubindende Komponente befindet sich selbst noch in Entwicklung und steht erst zu einem späterem Zeitpunkt mit den erforderlichen Funktionalitäten zur Verfügung.
    • Die einzubindende Komponente hat noch kein stabiles Verhalten. Anstatt das eigentliche Testobjekt zu überprüfen muss man ggf. erst entsprechende Fehleranalysen für die eingebundene Komponente durchführen.
    • Die einzubindende Komponente benötigt ihrerseits eine Reihe von Kommunikationspartnern
  • Möglichkeit der Ansteuerung und Verifikation
    Häufig ist es erforderlich und sinnvoll zumindest einige Aspekte der Kommunikation zwischen Testobjekt und Kommunikationspartnern zu überprüfen. Das bedeutet:
    • testrelevante Fehlerfälle können nachgestellt werden, so kann es z.B. erforderlich sein, dass ein Kommunikationspartner des Testobjektes so parametriert wird, dass er ein bestimmtes (Fehl-) Verhalten zeigt, wenn er vom Testobjekt angesprochen wird
    • alle test relevanten Aspekte der Kommunikation zwischen Kommunikationspartnern und Testobjekt sind im Test überprüfbar (z.B. übergebene Parameter beim Schnittstellenaufruf)

Grundelemente eines typischen Testablaufes (siehe Abbildung)

  • Innerhalb eines Testfalles (test case) wird eine Schnittstellenfunktion des Testobjektes aufgerufen
  • Das Testobjekt delegiert zur Erfüllung der Anfrage bestimmte Teilaufgaben an seine Kommunikationspartner
  • Zur Verifikation eines korrekten Ablaufes sind dann innerhalb des Testfalles folgende Schritte möglich:
    • Unmittelbar prüfbare Aspekte des Aufrufes (Returnwert, Rückgabeparameter, nachfolgende Statusabfragen am Testobjekt selbst) können direkt verifiziert werden.
    • Die erfolgten Aufrufe in die Laufzeitumgebung müssen erfasst und ausgewertet werden

Top1.1.2 Begriffsdefinition: Mocks, Stubs, Dummies

Je nach Verwendung der simulierten Komponenten unterscheidet man in der Test-Literatur folgende Begriffe:

Dummy-Object

  • ein Dummy-Objekt wird innerhalb des Tests lediglich als Parameter weitergereicht
  • Methodenaufrufe erfolgen nicht, es sind deshalb keine Methodenrealisierungen erforderlich

Fake- oder Stub-Object

  • realisiert eine (stark vereinfachte) funktionsfähige Implementierung, die für Produktiv-Zwecke nicht geeignet ist
  • die Realisierung beschränkt sich auf die für den Testfall erforderlichen Funktionen
  • Informationen über die durchgeführten Aufrufe können aufgezeichnet und zur Abfrage durch übergeordnete Instanzen bereitgestellt werden

Mock-Object

  • wird "vorprogrammiert" mit den zu erwartenden Aufrufen durch das Testobjekt
  • führt die Verifikation des Kommunikationsablaufes in der "Verifikationsphase" eigenständig durch

Weiterführende Referenzen

Martin Fowler: Mocks aren't Stubs
Begriffsdefinitionen zu Stubs und Mockobjects, Gegenüberstellung der Verifikation des Zustandes gegenüber der Verifikation des Verhaltens.

Gerard Meszaros: xUnit Patterns: Mocks, Fakes, Stubs and Dummies
Begriffsdefinitionen, umfangreiche Darstellung der möglichen Strategien beim Testen

Top1.2 Zugriff auf das Testobjekt

Top1.2.1 Beschränkung auf öffentliche Interfaces (Blackbox-Test)

Für manche Tests ist es ausreichend, die regulären Schnittstellen des zu testenden Systems anzusteuern. Dies bedeutet, daß sowohl das "Triggern" des zu testenden Systems wie auch das "Beobachten" der erwarteten Ergebnisse bzw. Reaktionen über die öffentlichen Schnittstellen des zu testenden Systems erfolgen kann.

Für diese Fälle sind keine besonderen Vorkehrungen für den Zugriff auf das Testobjekt erforderlich.

Top1.2.2 Zugriff auf private Daten (Whitebox-Test)

Der Zugriff auf private Daten kann aus folgenden Gründen sinnvoll sein:
  • Setzen von Anfangsbedingungen für einen Testfall
    Durch den unmittelbaren Zugriff auf private Memberdaten können beliebige Zustände im zu testenden System leicht eingestellt werden. Auf "reguläre" Weise über die öffentlichen Schnittstellen sind diese Zustände oft nur mühsam herzustellen, denn dies ist oft verbunden mit:
    • Hochfahren des Gesamtsystems mit veränderten Konfigurationsdaten, dadurch entsprechend hoher Zeitbedarf für das Herunterfahren und anschliessende Hochfahren des Systems. Verwaltungsaufwand für die entsprechenden Konfigurationsfiles.
    • Aufwendiges Durchspielen ganzer Szenarien um eine gewünschte Vorbedingung herzustellen
  • Überprüfung interner Abläufe/Algorithmen im zu testenden System
    • Um die Korrektheit eines SW-Systems zu prüfen, ist es häufig sinnvoll auch interne Abläufe und Zustände zu überwachen. Meist ist die Kontrolle über die regulären öffentlichen Schnittstellen nur indirekt und unvollständig möglich.
    • Anwendungsbeispiel: Abfrage der Zustände einer Statemachine, die das Verhalten einer SW-Komponente bestimmt.
Randbedingungen:

Für das zu testende System sind folgende Eigenschaften wünschenswert:
  • der Zugriff auf Interna soll nur für den Test möglich sein
  • der reguläre Produktivcode soll zwar das Testen unterstützen aber dennoch möglichst keine spezifischen Anweisungen für den Test enthalten
Lösungsansatz: friend-Deklaration
// MyClassToTest.h (part of the system under test)

class MyClassToTest : public IMyClassToTest
{
    public:

      // Methods of public interface IMyClassToTest
      virtual void DoSomethingPublic (void);

    private:
        int m_internalState;

        // Allow full access for the purpose of testing
        friend class AccessForWhiteboxTest;
};
// MyTest.cpp (part of the test environment)

// Use the internal structure of the object under test
#include <TestObject/Internals/MyClassToTest.h>

class AccessForWhiteboxTest
{
    public:

    static void SetInternalState (
        IMyClassToTest* in_pIMyClass
        int             in_newState)
    {GetInternals (in_pIMyClass)-> m_internalState = in_newState;}

    static int GetInternalState (
        IMyClassToTest* in_pIMyClass)
    {return GetInternals (in_pIMyClass)-> m_internalState;}

    static MyClassToTest* GetInternals (IMyClassToTest* in_pIMyClass)
    {
        // Knowing the internal structure allows casting of the
        // public interface pointer to the specific class type
        return static_cast<MyClassToTest*>(in_pIMyClass);
    }
};
// MyTestCases.cpp (part of the test environment)

void MyTestFunction ()
{
    IMyClassToTest* pIMyClass = ... // get interface via the
                                    // "official" public access method

    // set some initial state
    AccessForWhiteboxTest::SetInternalState (pIMyClass , INITIAL_STATE);

    ...

    // after some interactions the state should have changed
    Verify (AccessForWhiteboxTest::GetInternalState (pIMyClass) == RUNNING);
}
Hinweis:
Der Beispielcode wurde absichtlich einfach gewählt. In der modernen Programmierpraxis sollten anstelle von Raw-Pointern besser Smart-Pointerklassen oder auch Referenzen eingesetzt werden.

Top1.2.3 Zugriff auf private Methoden (Whitebox-Test)

Neben dem Zugriff auf alle Attribute (siehe vorhergehender Abschnitt), kann es auch sinnvoll sein, private Methoden (oder allgemein Methoden ausserhalb des öffentlichen Interfaces einer Klasse) anzusteuern. Nimmt man eine übliche Organisation in getrennte Übersetzungseinheiten an (z.B. unter MS-Windows eine eigene DLL für jedes Teilsystem), so tritt dabei das Problem auf, dass nicht virtuelle Funktionen des zu testenden Systems in der Übersetzungseinheit der Testumgebung nicht zur Verfügung stehen.

Lösungsansatz: exportierte Zugriffsklasse

Die Klasse AccessForWhiteboxTest wird in die Übersetzungseinheit der zu testenden Komponente verlagert und dort als Klasse zur allgemeinen Verwendung exportiert. Da sie sich in der gleichen Übersetzungseinheit wie die zu testende Klasse befindet, sind alle Methoden und Daten prinzipiell zugreifbar.

// MyClassToTest.h (part of the system under test)

class MyClassToTest : public IMyClassToTest
{
    public:
      ...

    private:
        int m_internalState;

        // Allow full access for the purpose of testing
        friend class AccessForWhiteboxTest;

    private:
        void DoSomethingInternal (void);
};
// AccessForWhiteboxTest.h (part of the system under test)

#ifdef SYSTEM_UNDER_TEST_EXPORTS
#define SYSTEM_UNDER_TEST_API __declspec(dllexport)
#else
#define SYSTEM_UNDER_TEST_API __declspec(dllimport)
#endif

// Use the internal structure of the object under test
#include <TestObject/Internals/MyClassToTest.h>

SYSTEM_UNDER_TEST_API class AccessForWhiteboxTest
{
    public:

    static void DoSomethingInternal  (
        IMyClassToTest* in_pIMyClass
        int             in_newState)
    {GetInternals()-> DoSomethingInternal();}

    static void SetInternalState (
        IMyClassToTest* in_pIMyClass
        int             in_newState);
    static int GetInternalState (
        IMyClassToTest* in_pIMyClass)
    static MyClassToTest* GetInternals (IMyClassToTest* in_pIMyClass)
    {
        // Knowing the internal structure allows casting of the
        // public interface pointer to the specific class type
        return static_cast<MyClassToTest*>(in_pIMyClass);
    }
};
// MyTestCases.cpp (part of the test environment)

// Use the access object exported by the system under test
#include <TestObject/Internals/AccessForWhiteboxTest.h>

void MyTestFunction ()
{
    IMyClassToTest pIMyClass = ... // get interface via the
                                   // "official" public access method

    // call some private function which would not be available
    // by regular means
    AccessForWhiteboxTest::DoSomethingInternal(pIMyClass);

    ...
}

Top1.3 Dummy-Implementierungen

In vielen Projekten werden auch an Interfaces über einen längeren Zeitraum Änderungen durchgeführt. Um den ständigen Aktualisierungsaufwand für Testumgebungen in Grenzen zu halten kann nach folgendem Modell vorgegangen werden:

Annahmen und Randbedingungen:

  • Es werden häufig nur einige Methoden eines Interfaces während der Testausführung tatsächlich aufgerufen.
  • Für die Instanziierbarkeit eines Objektes müssen aber alle Methoden vorhanden sein.

Ansatz:

  • Die Originalkomponenten stellen Dummy-Implementierungen ihrer Interfaces zur Verfügung, die stets aktuell gehalten werden.
  • In den Testumgebungen werden statt der Original-Interfaces die Dummy-Implementierungen verwendet.
  • Vorteil:
    • es müssen nur die tatsächlich aufgerufenen Methoden realisiert werden
    • alle anderen Methoden passen sich stets an geänderte Signaturen und sonstige Änderungen "automatisch" an

Dummy-Implementierung eines Interfaces:

// ISomeInterfaceDummyImpl.h (part of the system under test)

// DummyImplementation of interface ISomeInterface 
struct ISomeInterfaceDummyImpl : public ISomeInterface
{
    virtual int GetSomeInt()              {TEST_DUMMY_IMPL;}
    virtual void DoSomething (int in_val) {TEST_DUMMY_IMPL;}
    virtual void DoSomethingElse()        {TEST_DUMMY_IMPL;}
    ...
};

// CheckDummyImpl.cpp (part of the system under test)

class TestISomeInterfaceDummyImpl : public ISomeInterfaceDummyImpl
{};

... (classes for other dummy interfaces)

// Verify that all dummy implementations can be instantiated
//
// Remark: This function is never called. Its only purpose is
// that the compiler checks that the classes deriving from the
// dummy implementations can be instantiated. A compile error
// in this function usually is caused by changes to some interface.
// With the compile error the developer is reminded to adjust also
// the dummy implementation of the interface.
void TestDummyImplementationsOfInterfaces
{
    new TestISomeInterfaceDummyImpl;

    ... (other test instantiations)
}

Makro TEST_DUMMY_IMPL:

// Makro TEST_DUMMY_IMPL
// Produce hint that the dummy implementation is used.
// Debug version   : assert
// Release version : throw std::exception
// Remark:
// If this macro is called within a unit test, then the corresponding
// method needs to be implemented within the specific test environment.
#define TEST_DUMMY_IMPL                                              \
    assert(!\"Do not use dummy implementation!\");                   \
    throw std::exception(\"Do not use dummy implementation for \")   \
    +   std::string(__FUNCTION__)), __FILE__, __LINE__, __FUNCTION__)   

Anwendung bei der Realisierung eines FakeObjects:

class MyFakeObject : public ISomeInterfaceDummyImpl
{
public:

  // Only realize the methods which are really used within the test
  virtual int GetSomeInt()
  {return 17;}
};
Durch die Ableitung von der Dummy-Implementierung des Interfaces ist gewährleistet, dass das FakeObject stets instanziiert werden kann, selbst wenn für das zugrunde liegende Interface Änderungen vorgenommen werden.

Top2 Verifikation und Teststeuerung

Top2.1 Zentralisierte Erfassung der "Testereignisse" (TestEvents)

Im Unterschied zur Verwendung mehrerer spezifischer Mock-Objekte, die das beobachtete Verhalten jeweils aus ihrer Sicht eigenständig überprüfen, wird im Folgenden versucht, die Verifikationslogik zu verallgemeinern und in eine übergreifende, zentrale Komponente auszulagern, die unabhängig von der spezifischen Testumgebung eingesetzt und wiederverwendet werden kann.

Top2.1.1 Prinzipieller Mechanismus

Es werden dabei folgende Prinzipien verfolgt:

  • Text-Ereignisse
    einfaches Realisierungsprinzip: ein Ereignis entspricht einem einzeiligen Text, der alle relevanten Informationen (Methodenaufruf, Parameter, meldende Klasse/Komponente, Auflistung relevanter Werte etc.) enthält. Der Umfang der Informationen wird vollständig vom Autor der spezifischen Testumgebung bestimmt.
  • Singleton, von überall erreichbar
    In der gesamten Testumgebung gibt es genau eine Instanz der Klasse TestEvents. Entsprechend dem Singleton-Prinzip ist diese Instanz erreichbar für:
    • Fake-Objekte, die Aufrufe durch das Testobjekt weitermelden
    • Test cases, die die Verifikation durch Überprüfung der eingetragenen "Testereignisse" durchführen

Schematische Darstellung:

TestEvents

Hinweis:
Das vorgeschlagene Konzept der TestEvents hat Ähnlichkeiten mit dem Konzept des "TestSpy" aus xUnit Patterns: Test Spy

Weitergehende Möglichkeiten

Da alle relevanten Testereignisse an die zentrale Framework-Komponente "TestEvents" geleitet werden, ergeben sich auf einfache Weise die folgenden zusätzlichen Erweiterungsmöglichkeiten:

  • Testprotokoll, Logfile
    Die Einträge in TestEvents können in ein Logfile geleitet werden, das einerseits als formaler Beleg für die Durchführung des Tests oder auch als Grundlage für die Analyse aufgetretener Fehler dienen kann.
  • Traces
    In ähnlicher Weise können Traceausgaben generiert werden, die in Verbindung mit den regulären Traceausgaben des Testobjektes den Kommunikationsablauf des Gesamtsystems widerspiegeln.
  • Fehleranalysen
    Tritt ein erwartetes Testereignis nicht ein, so ist es i.d.R. für die Analyse hilfreich, die bis dahin aufgezeichneten Testereignisse zusammen mit der Information, ob sie erwartet wurden oder nicht, abrufen zu können.

Top2.1.2 Simulation der Fake-Objects

Häufig ist es ausreichend, im Test lediglich den Aufruf eines bestimmten Objektes aus der Laufzeitumgebung des Testobjektes nachzuweisen. Für diese Fälle beschränkt sich die Simulation des FakeObjects auf einfache Methodenimplementierungen nach folgendem Muster:
void FakeObject1::DoSomeService (void)
{
    // simply log info about call as a line of text
    TheTestEvents()->AddEvent("DoSomeService (called at FakeObject1)");
}

In manchen Situationen ist es erforderlich, das Kommunikationsverhalten des FakeObjects abhängig vom Testfall zu beeinflussen, um entsprechende Rückwirkungen auf das TestObjekt auslösen zu können.
Ausserdem kann es zur Vereinfachung der Verifikation sinnvoll sein, dass je nach Testfall andere Aspekte / andere FakeObjects überwacht werden. Das erreicht man am einfachsten dadurch, dass man einem FakeObject mitteilen kann, ob es seine Aktivitäten als Testereignisse weitermelden soll:

class FakeObject2
{
public:

    //-----  interface method called by the TestObject -----

    int DoSomeOtherService (int in_param)
    {
        // (1) log info about call as a line of text
        if (m_logActivitiesToTestEvents)
        {
            TheTestEvents()->AddEvent("DoSomeOtherService in=",
                in_param, " (called at FakeObject2)");
        }

        // (2) configurable communication behaviour of FakeObject
        return m_retVal;
    }

    //-----  control methods called by the test case -----

    void SetRetVal (int in_retVal)
    {m_retVal = in_retVal;}

    void SetLogActivities (bool in_logActivities)
    {m_logActivitiesToTestEvents = in_logActivities;}

private:
    // configurable return value, visible to TestObject
    int m_retVal;

    // flag to activate/deactivate generation of TestEvents 
    bool m_logActivitiesToTestEvents;
};

Top2.1.3 Formulierung der Testfälle (Test cases)

Ein Testfall besteht typischerweise aus einer oder mehrerer Teilsequenzen nach folgendem Muster:
  • Vorbereitung (Schritt 1 im Diagramm)
    Vorbereitung der Testumgebung, Instrumentierung der Fake-Objects, damit das TestObject bei nachfolgender Anregung die benötigten Reaktionen erfährt
  • Anregung des Testobjektes (Schritt 2 im Diagramm)
    Aufruf des Testobjects über reguläre Schnittstellen (Blackbox-Test) oder spezifische Testschnittstellen (Whitebox-Test).
    • Das Testobjekt ruft seinerseits Kommunikationspartner innerhalb seiner Laufzeitumgebung (Schritt 3 im Diagramm)
    • Handelt es sich dabei um Fakeobjects, so kann der Aufruf in der zentralen Komponente TestEvents aufgezeichnet werden (Schritt 4 im Diagramm)
  • Verifikation (Schritt 5 im Diagramm)
    Prüfung des aufgetretenen Kommunikationsverhaltens des Testobjects mit seiner Umgebung

Auszug aus einem Testcase:

// prepare environment
fakeObject2->SetRetVal(5);
fakeObject2->SetLogActivities(true)

// call test object
myTestObject->SomeInterfaceMethod();

// check whether the call has triggered the following reactions in the given order
TheTestEvents()->Expect("DoSomeService (called at FakeObject1)");
TheTestEvents()->Expect("DoSomeOtherService in=3 (called at FakeObject2)");

Top2.2 Multithreading, Synchronisation paralleler Abläufe

Existieren in der Laufzeitumgebung des Testobjektes parallele Abläufe (Multithreading), so kann die Reaktion des Testobjektes verzögert werden. Für den Test bedeutet das, dass bei Rückkehr des Aufrufs einer Schnittstelle des Testobjekts, die Bearbeitung der Anfrage noch gar nicht abgeschlossen sein muss. Die asynchrone Reaktion verhindert somit eine unmittelbare Überprüfung der erwarteten Reaktionen.

Top2.2.1 Naiver Ansatz: Warten mit Sleep

Ein erster Ansatz könnte darin bestehen, nach derartigen Aufrufen einfach eine vorgegebene Zeit abzuwarten und dann erst im Testscript mit den nächsten Aktionen (z.B. Prüfung der dann hoffentlich schon eingetretenen Reaktionen) fortzufahren. Abgesehen von sehr einfachen Anwendungsfällen ergeben sich mit dieser Vorgehensweise häufig folgende Probleme:
  • unsichere Synchronisation
    je nach Leistungsfähigkeit und sonstiger Auslastung des Rechners, auf dem der Test gerade ausgeführt wird, kann die erforderliche Wartezeit stark variieren. Bei starker Nebenlast (z.B. durch andere parallele Test/Compilier- Aktivitäten) kann die Wartezeit zu niedrig sein. Das Testscript wird fortgesetzt bevor die gerade angestossene Aktivität vollständig bearbeitet wurde. Der Test wird dadurch i.d.R. scheitern.
  • vervielfachte Ausführungsdauer
    Tests enthalten meist eine ganze Reihe von Testfällen, sodass sich die Wartezeiten addieren. Verzögerungen um den Faktor 10 bis 50 durch die eingeführten Wartezeiten sind dabei durchaus zu erwarten. Häufig ist das für regelmäßig auszuführende Tests (z.B. direkt im Anschluß an die Compilierung) nicht mehr akzeptabel.

Top2.2.2 Gezielte Synchronisation

Die Reaktionen des Testobjektes bestehen sehr häufig darin, dass Kommunikationspartner aus der Laufzeitumgebung des Testobjektes angesprochen werden. Handelt es sich dabei um simulierte Fake-Objects, so können innerhalb dieser Objekte Synchronisationsmechanismen eingesetzt werden.

Grundlegendes Ziel dabei ist es, die ggf. verzögerten Reaktionen des Testobjektes mit dem sequentiellen Ablauf der einzelnen Testinteraktionen im Testscript (test case) zu synchronisieren.

Schematische Darstellung:

Synchronisation paralleler Abläufe

Ein Beispiel für eine technische Umsetzung eines Synchronisationsmechanismus findet sich unter TestToolBox-Synchronizer.

Top2.2.3 Simulation der Fake-Objects

In Erweiterung der weiter oben aufgeführten Realisierungsbeispiele ist es meist ausreichend, lediglich einen Synchronisationsaufruf zu ergänzen:
void FakeObject1::DoSomeService (void)
{
    // simply log info about call as a line of text
    TheTestEvents()->AddEvent("DoSomeService (called at FakeObject1)");

    // generate synchronisation event
    TheSynchronizer()->GenerateSync()");
}

Top2.2.4 Formulierung der Testfälle (Test cases)

Ein Testfall besteht typischerweise aus einer oder mehrerer Teilsequenzen nach folgendem Muster:
  • Vorbereitung (Schritt 1 im Diagramm)
    Im Testfall muss bekannt sein, wieviele Synchronisationsereignisse abgewartet werden müssen, bevor die Testsequenz fortgesetzt werden kann
  • Anregung des Testobjektes (Schritt 2 im Diagramm)
    Aufruf des Testobjects über reguläre Schnittstellen (Blackbox-Test) oder spezifische Testschnittstellen (Whitebox-Test).
  • Synchronisiertes Warten (Schritt 3 im Diagramm)
    Die Ausführung des sequentiellen Testskriptes wird angehalten bis alle erwarteten asynchronen Ereignisse eingetreten sind.
  • Asynchrone Aktivitäten des Testobjektes
    • Das Testobjekt ruft seinerseits Kommunikationspartner innerhalb seiner Laufzeitumgebung (Schritt 4 im Diagramm)
    • Handelt es sich dabei um Fakeobjects, so kann der Aufruf in der zentralen Komponente TestEvents aufgezeichnet werden. Zusätzlich werden entsprechende Synchronisationsereignisse über die zentrale Komponente Synchronizer ausgelöst. (Schritt 5 im Diagramm)
  • Abschluss der Synchronisation und Verifikation (Fortsetzung Schritt 3 im Diagramm)
    Nach Eintreffen aller erwarteten Synchronisationsereignisse erfolgt die Prüfung des aufgetretenen Kommunikationsverhaltens des Testobjects mit seiner Umgebung.

Auszug aus einem Testcase:

// prepare synchronization
// here we have to wait for 2 asynchronous reactions
TheSynchronizer->Init(2);

// now call test object
myTestObject->SomeInterfaceMethod();

// wait until sync events have been received
TheSynchronizer()->WaitForSync();

// synchronization point is passed, now we can proceed
// with the test sequence

// check whether the call has triggered the following reactions in the given order
TheTestEvents()->Expect("DoSomeService (called at FakeObject1)");
TheTestEvents()->Expect("DoSomeOtherService in=3 (called at FakeObject2)");

Top3 Kontrollierte Testausführung

Top3.1 Zeitbeschränkte Ausführung (TimedExec)

Automatisierte SW-Tests werden typischerweise auch automatisiert gestartet und in übergeordnete Abläufe eingebettet.

Beispiele:

  • Ausführung und Auswertung einer Reihe von UnitTests zu allen Teilsystemen/Units eines SW-Systems
  • Einbettung in den Buildprozeß
    In größeren SW-Projekten wird für die Erzeugung des Gesamtproduktes aus den einzelnen Teilprodukten ein oft länger dauernder SW-Generierungsprozeß benötigt. Um eine laufende Konsistenzprüfung des aktuellen SW-Standes durchführen zu können und Entwickler und Tester mit "frischen" Ergebnissen versorgen zu können, werden regelmäßig automatische Gesamtbuilds durchgeführt (z.B. nächtliche Generierung). Neben dem Produktivcode werden selbstverständlich auch die zugehörigen Testumgebungen mit generiert. Nach dem reinen Compilieren/Linken macht es dabei durchaus Sinn auch gleich die Testausführung in den Generierablauf mit einzubinden (Beispiel MS Visual Studio: Start der Testausführung als Postbuild-Step, Verwendung des Testergebnisses als Returncode für den Buildprozeß)

Top3.1.1 Womit zu rechnen ist: Tests schlagen fehl!

In einem lebendigen Projekt werden Tests häufig Laufzeitfehler zeigen:

  • Geänderte Interfaces
    Änderungen an Interface-Methoden können dazu führen, dass nicht mehr die vorgesehenen Methoden der Fake-Objekte aufgerufen werden, sondern statt dessen die Dummy-Implementierungen, die nicht für einen tatsächlichen Aufruf gedacht sind (siehe Dummy-Implementierung von Interfaces).
  • Weiterentwicklung verwendeter Originalkomponenten
    In den Test als Teil der Laufzeitumgebung eingebundene Originalkomponenten können im Zuge der Weiterentwicklung ihr Verhalten ändern.
  • Weiterentwicklung Testobjekt
    Die Implementierung des Testobjektes selbst kann sich ändern und deshalb an zunächst nicht vorhersehbarer Stelle zu verändertem Verhalten im Testablauf führen

Diese "Fehlschläge" sind durchaus erwünscht. Denn ein wesentliches Testziel ist das Erkennen von Veränderungen in der SW. Ein Test der vermeintlich robust gegenüber SW-Änderungen ist, d.h. der nicht angepasst werden muss, wenn sich wesentliche Aspekte der Realisierung ändern, hat mit großer Wahrscheinlichkeit auch den Nachteil, daß mit ihm auch unerwünschte Änderungen nicht erkannt werden können.

Top3.1.2 Möglichst vermeiden: Blockade des Gesamtablaufes

Das Fehlverhalten eines Testes kann zu einer dauerhaften Blockade im Ablauf führen, in den der Test eingebettet ist:
  • Zu bestätigende Assert-Box
  • Absturz mit Hängenbleiben des Testprozesses in einer System-Dialogbox
  • Hängenbleiben in Endlosschleife
  • Deadlock aufgrund falscher Synchronisation
Es ist zwar wünschenswert, dass das Fehlverhalten aufgedeckt wird. Wichtig ist es aber andererseits auch, dass die dem Test nachfolgenden Aktionen (z.B. Tests weiterer Systemkomponenten, Compilierung nachfolgender (Test-)Projekte) dadurch nicht behindert werden

Hinweis: Es gibt Testumgebungen, die beispielsweise Exceptions abfangen können und den Testablauf dann von der Ausnahme "unbeeindruckt" korrekt fortsetzen oder beenden können. In der Regel versagt diese Methode jedoch in komplexeren Systemen, in denen die Ausnahmen in unterschiedlichen Workerthreads auftreten können.

Top3.1.3 Ansteuerung des Assert- und Fehlerbox-Verhaltens

Default-Verhalten für Assert
Im Quellcode können beliebige logische Bedingungen als assert-Anweisung formuliert werden:
#include <assert.h>
...
assert(numEntries >= 5);
...
assert(!"Some condition is not fulfilled");
Ist die logische Bedingung nicht erfüllt, so wird im Falle einer Konsolenaplikation die Information über die fehlgeschlagene Bedingung mit Angabe zur betroffenen Code-Stelle nach stdout/stderr geschrieben. Zusätzlich wird ein modaler Dialog angezeigt und der Programmablauf hält bis zur Bestätigung an:

assert message box

Für Windowsapplikationen wird die gescheiterte Bedingung direkt in der Dialogbox angezeigt.

Default-Verhalten für typische fatale Fehler
Klassische Fehler wie z.B. Nullzeigerzugrifff oder Teilen durch 0 führen zum Programmabbruch und zur Anzeige entsprechender modaler Dialogboxen, die den weiteren Ablauf anhalten.

Zugriff auf Nullzeiger:

access violation message box

Teilen durch 0:

division by zero message box

Verändern des Defaultverhaltens
Das Defaultverhalten zur Meldung und Anzeige der beschriebenen Fehler kann gezielt eingestellt werden. Zur Durchführung von UnitTests erscheinen folgende Massnahmen sinnvoll:
  • Umleitung der Meldungen zu Warnungen, Fehlern und Asserts über _CrtSetReportMode sowohl in die Standardausgabe (_CRT_MODE_FILE mit _CRTDBG_FILE_STDOUT als gesetztem Reportfile) als auch in das Ausgabefenster eines laufenden Debuggers (_CRT_MODE_DEBUG). Weitere Details
  • Wird die Applikation zur Fehleranalyse im Debugger betrieben, so ist es sinnvoll, dass zum schnelleren Erkennen der gescheiterten Codestelle die Fehlerboxen aktiviert werden, d.h. der Reportmode _CRTDBG_MODE_WNDW sollte dann eingesclaltet werden. Zur automatischen Erkennnung, ob die Anwendung im Debugger betrieben wird, kann die Windows-Funktion IsDebuggerPresent() verwendet werden.
  • Es kann auch sinnvoll sein, das gewünschte Verhalten über die Kommandozeile gezielt ein- und ausschalten zu können. Sind die Aufrufparameter nicht direkt über die Main-Funktion verfügbar, so kann an beliebiger Stelle im Quellcode über die Windows-Funktion GetCommandLine() auf die Kommmandozeilen-Parameter zugegriffen werden. Weitere Details
Code-Beispiel
Die Syntax der Kommandozeilenparameter ist spezifisch für jede Applikation. Im nachfolgenden Beispiel wird angenommmen, dass IsCmdLineArgExisting() und GetCmdLineArg() als spezifische Funktionen vorhanden sind.
// Initially deactivate display of
// fatal error or assert message boxes
int displayErrorMsgBox = 0;

// First check command lline
if (IsCmdLineArgExisting("ShowFatalErrorMsgBox"))
{
    if (GetCmdLineArg("ShowFatalErrorMsgBox")=="YES")
    {
        displayErrorMsgBox = _CRTDBG_MODE_WNDW;
    }
}
else // not set via command liine
{
    if (IsDebuggerPresent())
    {
        displayErrorMsgBox = _CRTDBG_MODE_WNDW;
    }
}

// Send all reports to STDOUT and debugger output
_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_FILE|_CRTDBG_MODE_DEBUG );
_CrtSetReportFile( _CRT_WARN, _CRTDBG_FILE_STDOUT );
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_FILE|_CRTDBG_MODE_DEBUG
				             | displayErrorMsgBox );
_CrtSetReportFile( _CRT_ERROR, _CRTDBG_FILE_STDOUT );
_CrtSetReportMode( _CRT_ASSERT, _CRTDBG_MODE_FILE|_CRTDBG_MODE_DEBUG
				             | displayErrorMsgBox);
_CrtSetReportFile( _CRT_ASSERT, _CRTDBG_FILE_STDOUT );
Fatale nicht abschaltbare Endemeldungen
Durch die oben aufgeführten Parametrierungen wird die Windows-Meldung zum fatalen Abbruch eines Programms nicht beeinflusst.

fatal termination message box

Der weitere Ablauf wird durch diese Meldung weiterhin blockiert. Um einen ungehinderten Ablauf für diesen Fall und auch für die weiter oben beschriebenen logischen "Hänger" zu gewährleisten, wird im nachfolgenden Abschnitt eine übergeordneter Lösungsansatz vorgesteellt.

Top3.1.4 Einführung einer zusätzlichen Kontrollinstanz

Eine Testapplikation wird nicht direkt zur Ausführung gebracht, sondern über einen zusätzlichen Indirektionschritt einer einfachen Überwachungsinstanz zur Ausführung übergeben, der die maximal zulässige Ausführungszeit mitgeteilt wird. Wird die konfigurierte Ausführungszeit überschritten, so wird der Testprozess von der Überwachungsinstanz abgebrochen und ein Fehlercode wird an den umgebenden Prozess zurückgegegen, der dann z.B. mit dem nächsten Testprojekt fortsetzen kann.

Technische Realisierungsmöglichkeit: Einfache Konsolenapplikation, die über die Kommandozeile mit dem zu startenden Test-Executable und der erlaubten Ausführungszeit versorgt wird. Details siehe TestToolBox-TimedExec.

Bei Bedarf lassen sich folgende zusätzlichen Funktionalitäten über die eingeführte Überwachungsinstanz lösen:

  • Synchronisation konkurrierender Zugriffe durch Testapplikationen
    Moderne Rechner und Buildsysteme übersetzen stets mehrere Teilprojekte parallel. Damit können auch die in den Buildprozeß eingebundenen Testapplikationen parallel gestartet werden. Greifen diese konkurrierend auf begrenzte exklusive Ressourcen zu (z.B. Portadressen), so kann über die Überwachungsinstanz auf einfache Weise eine Serialisierung der Testausführung durchgeführt werden. Innerhalb der spezifischen Testumgebungen kann dann der exklusive Zugriff auf die Systemressourcen einfach vorausgesetzt werden.
  • Zentrale Erfassung der Testergebnisse
    Die Überwachungsinstanz kennt alle gestarteten Testapplikationen und ihr Laufzeitverhalten (Returncode bzw. Notwendigkeit des gezielten Beendens nach Hängenbleiben). Es ist somit ein leichtes daraus ein einfaches Übersichtsprotokoll zu den durchgeführten Testläufen zu erstellen.
  • Zentrales Ein/Ausschalten der Testausführung
    Je nach persönlichen Präferenzen oder nach den Erfordernissen für die spezifische Generierumgebung kann über das Setzen einer Umgebungsvariablen das Starten der Tests abgeschaltet werden, ohne dass in irgendeinem Teilprojekt eine Veränderung vorgenommen werden muß.
Schematische Darstellung (Einbettung in Buildprozess):

Sichere Ausführung von Tests im Buildprozeß

Top3.2 Wiederholte Ausführung von Tests

Top3.2.1 Robustheit: Wiederholung beliebiger Testapplikationen

In der Praxis gibt es gelegentlich Situationen, in denen es zum sporadischen Fehlschlag eines ansonsten "meist" korrekt arbeitenden Testes kommt. Häufig treten diese Probleme im Multithreaded-Umfeld auf und sind z.B. durch Prozessor-Nebenlasten oder durch unsichere Synchronisationsmechanismen im Testablauf bedingt.

Um die allgemeine Stabilität und Reproduzierbarkeit des Testablaufes zu untersuchen, ist es daher sinnvoll, eine Testanwendung einem Stresstest zu unterziehen und sie in einer Endlosschleife immer wieder auszuführen. Signalisiert die Testapplikation den Fehlschlag über einen entsprechenden Exitcode, so kann die Testausführung im Fehlerfall gezielt angehalten werden. Auf diese Weise bleiben für die Fehleranalyse die letzten Logdateien und die Traceinformationen, die dem Fehler unmittelbar vorausgehen, erhalten.

Beispiel für ein einfaches CMD-Script zur Ansteuerung der Testausführung:

@echo OFF
:: File: Repeat.cmd
:: Syntax: Repeat [targetDir] [exeFile] [cmdLineOptions]
::
:: Call unit test in an endless loop and stop execution when
:: the first error is detected

setlocal
set targetDir=C:\\MyTargetDir\\bin
set exeFile=MyTestApp.exe
set options=-formal
set counter=0
if \"%1\" NEQ \"\" set targetDir=%1
if \"%2\" NEQ \"\" set exeFile=%2
if \"%3\" NEQ \"\" set options=%3

echo.
echo Repeated execution of unit test (stop when first error is found)...
echo.
echo ------------------------------------------------------
echo Target directory : %targetDir%
echo Test executable  : %exeFile%
echo Cmdline options  : %options%
echo ------------------------------------------------------
echo.
goto EXEC_TEST

:: loop for repeated execution
:EXEC_TEST
set /a counter+=1
echo Executing iteration %counter%

%targetDir%\\TimedExec.exe 120  %targetDir%\\%exeFile% %options% >nul
@if errorlevel 1 GOTO STOP_TEST
GOTO EXEC_TEST

:: reaction in case of error
:STOP_TEST
echo.
echo Error detected! Test has stopped!
echo Check logfiles and / or trace output to analyze error situation!

endlocal

Es können auch mehrere Testapplikationen auf einmal gestartet werden:

@echo OFF
:: File: RepeatAll.cmd

set targetDir=D:\\MyTargetDir\\bin
if \"%1\" NEQ \"\" set targetDir=%1

::start several test applications
for %%X in (MyTestAppA.exe MyTestAppB.exe MyTestAppC.exe)
    do start cmd.exe /k Repeat.cmd %targetDir% %%X

Top3.2.2 Ressourcenverbrauch: Wiederholung über das Testrahmensystem

Testumgebungen besitzen in der Regel ein steuerndes Rahmensystem, über das die einzelnen Testfälle ausgeführt werden. Über den bloßen Aufruf der Testfälle hinaus erscheinen folgende Ansteuerungsmöglichkeiten als sinnvoll:

  • Selektion der Testfälle
    Bei Bedarf Auswahl einer Teilmenge der Testfälle, z.B. nach Änderungen um bestimmte Problemsituationen/Fehler gezielt nachzustellen, ohne durch die Ausführungszeit nicht relevanter Testfälle aufgehalten zu werden.
  • Auswahl der Reihenfolge
    Je nach Organisation der Testfälle kann die Einhaltung einer bestimmten Reihenfolge zwingend sein. Verfolgt man das Paradigma, dass die einzelnen Testfälle unabhängig voneinander sind, so kann es sinnvoll sein zur Absicherung der Annahmen eine Zufallsreihenfolge zu erzeugen.
  • Dauertests
    Für Langzeituntersuchungen können die definierten Testfälle über einen längeren Zeitraum wiederholt ausgeführt werden.
    • Test der allgemeinen Systemstabilität
      Insbesondere bei teilweise nicht deterministischem Verhalten (z.B. im Multithreading-Umfeld) kann es sinmvoll sein, die Testfälle mehrfach auszuführen um die Entdeckungswahrscheinlichkeit für sporadisch auftretende Probleme zu erhöhen.
    • Untersuchung des Ressourcenverbrauchs
      Eine Einbettung der Abfrage relevanter Performance-Daten in den Testablauf hat den Vorteil, dass zu stets gleichen Randbedingungen (z.B. nach Ausführung des jeweils letzten Testfalls einer Iteration) der Ressourcenverbrauch bestimmt wird und eine Beeinflussung durch sporadische Schwankungen während der Testfallausführung vermieden wird.
Schematische Darstellung:

ausführendes System

Anmerkung zu Ressourcen-Leaks und Ressourcen-Verbrauch

Ein beliebtes Vorgehensmodell bei der Ausführung von Testfällen ist:

  • Um für verschiedene Testfälle gleiche Anfangsbedingungen zu schaffen, wird einfach das zu testende System jeweils neu instanziiert.
  • Prüfung auf Memory-Leaks bei Beendigung der Testapplikation
    Ein einfacher Check wird bereits durch die C-Runtime-Bibliothek angeboten (_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF)). Im Fehlerfall werden Speicherleaks ohne weiteren Programmieraufwand z.B. in das Debugfenster geschrieben.

Für den realen Betrieb gilt dagegen folgendes:

  • Von der Applikation nicht freigegebene Ressourcen werden i.d.R. automatisch vom Betriebssystem bei Prozessende wieder zur Verfügung gestellt.
  • Applikationen sind typischerweise über einen längeren Zeitraum in Betrieb. Das bedeutet, dass Funktionalitäten auch wiederholt aufgerufen werden können, ohne dass die Applikation "frisch" gestartet wurde.
  • Für einen geregelten Ressourcenverbrauch ist es daher auch wichtig, dass während des laufenden Betriebs nicht zuviele Ressourcen beansprucht werden.

Schlussfolgerungen für einen realistischen Dauertest

  • Es ist sinnvoll das Hochfahren und Herunterfahren des zu testenden Objektes von der Wiederholungsschleife über die Testfälle zu trennen
  • Man muss unterscheiden zwischen einmaligen Initialisierungen und ggf. wiederholten Initialisierungen/Vorbereitungen für die einzelnen Testfälle.

Ein Beispiel für eine technische Umsetzung eines Test-Ausführungssystems, das die genannten Eigenschaften aufweist, findet sich unter TestToolBox-Runner.

Top3.3 Prüfung auf Memory Leaks

Top3.3.1 Runtime Library zeigt Speicher-Lecks beim Programmende

Instrumentierungen im Quellcode
Durch einfache Instrumentierung der Runtime-Library werden Memory Leaks beim Beenden des Programms automatisch ausgegeben. Hierzu sind folgende Anweisungen notwendig.

Notwendiges Include:

#include <crtdbg.h>

Beim Hochfahren der Applikation (z.B. in main()) wird die Leakerkennung eingeschaltet und die Anzeige erkannter Leaks wird sowohl in den Standard-Output als auch in die verwendete Debug-Console umgelenkt:

// Send all reports to STDOUT and debugger output
_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_FILE |_CRTDBG_MODE_DEBUG );
_CrtSetReportFile( _CRT_WARN, _CRTDBG_FILE_STDOUT );
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
_CrtSetReportFile( _CRT_ERROR, _CRTDBG_FILE_STDOUT );
_CrtSetReportMode( _CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
_CrtSetReportFile( _CRT_ASSERT, _CRTDBG_FILE_STDOUT );

// Switch on detection of memory leaks
_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG));
Hinweis: Um das Aufblenden blockierender modaler Dialogboxen zur Anzeige fataler Fehler zu unterdrücken siehe Codebeispiel.

Im Fehlerfall werden Leaks in folgendem Format angezeigt:

Detected memory leaks!
Dumping objects ->
{92} normal block at 0x00033478, 4 bytes long.
 Data: <    > CD CD CD CD 
Object dump complete.

Auffinden der zugehörigen Codestelle
Zu jedem Leak wird eine sogenannte Allokationsnummer angezeigt (im Beispiel "92"). Ist der Fehler reproduzierbar, d.h. wird bei mehreren Testläufen stets die gleiche Allokationsnummer ausgegeben, so bestehen gute Chancen, die Stelle im Quellcode aufzuspüren, an der der Speicher angefordert wurde. Hierzu muss beim Hochfahren der Applikation für die entsprechende Allokation ein "Haltepunkt" gesetzt werden. Um dies möglichst einfach für verschiedene Allokationsnummern ohne Neucompilierung durchführen zu können empfiehlt sich Code nach folgendem Muster:
int allocNum = 0; //< set specific value within debugger!
if (allocNum!=0)
{
    _CrtSetBreakAlloc(allocNum);
}

Einige Test-Frameworks unterstützen die beschriebenen Leistungsmerkmale und erlauben darüberhinaus das Setzen eines Haltepunktes für eine bestimmte Allokationsnummer über die Kommandozeile (siehe Boost TestLib und TestToolBox-Runner).

Achtung: Debug-Version einsetzen!
Die beschriebenen Mechanismen der Runtime Library wirken nur in der Debug-Version. Bei Tests mit der Release-Version werden keine Memoryleaks angezeigt. Aus diesem Grunde sollte in jedem Falle auch die Debug-Version der Testapplikation generiert und getestet werden.

Top3.3.2 Automatische Erkennung der Leak-Situation

Die im vorhergehenden Abschnitt beschriebene Ausgabe von Memoryleaks nach Programmende hat einen Nachteil: Leaks werden erst nach Beendigung des Programms festgestellt und können somit nicht mehr zur Festlegung des Exitcodes des Programmes verwendet werden. Testapplikationen, die funktional alle Testfälle erfüllen, aber Teile des Speichers nicht mehr freigeben, werden deshalb typischerweise einen Exit-Code von 0 zurückgeben. Leaks werden also nur erkannt, wenn ein Tester "anwesend" ist, der den Testoutput begutachtet und gezielt nach entsprechenden Hinweisen auf Memoryleaks sucht.

Um automatisierte Testläufe in die Lage zu versetzen, auch derartige Leaks sicher erkennen zu können, wird ein zusätzlicher Indirektionsschritt eingeführt, der die "Inspektion" des Programm-Outputs automatisiert ausführt. Im einzelnen werden dazu folgende Schritte durchgeführt:

  • die Testapplikation wird nicht direkt, sondern über eine steuernde Applikation gestartet
  • die steuernde Applikation lenkt alle Ausgaben der Testapplikation, die normalerweise nach stdout und stderr gehen in eine Datei um
  • nach Beendigung der Testapplikation prüft die steuernde Applikation den "aufgefangenen" Output und prüft ihn auf das Vorhandensein des Textes "Detected memory leaks!"
  • wird dieser Text gefunden, so kann der ExitCode gezielt gesetzt werden, um einen Fehler zu signalisieren
Code-Snippets
Anlegen des Textfiles, das den Output der Testapplikation aufnehmen soll:
std::string nameApplicationOutputFile = GetFullPathOfTestApplication()
    + ".AppOut.txt";

SECURITY_ATTRIBUTES security;
security.bInheritHandle=true;
security.lpSecurityDescriptor=NULL;
security.nLength = sizeof(SECURITY_ATTRIBUTES);

HANDLE hApplicationOutputFile = CreateFile(
    nameApplicationOutputFile.c_str(),
    GENERIC_WRITE, 
    FILE_SHARE_WRITE, 
    &security, 
    CREATE_ALWAYS, 
    FILE_ATTRIBUTE_NORMAL, 
    0); 
Starten der Testapplikation mit Umlenken von StdOut und StdErr:
// Definition of startinfo struct
STARTUPINFO startInfo;
startInfo. cb              = sizeof (STARTUPINFO);
startInfo. lpReserved      = 0;
startInfo. lpDesktop       = 0;
startInfo. lpTitle         = 0;
startInfo. dwX             = 0;
startInfo. dwY             = 0;
startInfo. dwXSize         = 0;
startInfo. dwYSize         = 0;
startInfo. dwXCountChars   = 0;
startInfo. dwYCountChars   = 0;
startInfo. dwFillAttribute = 0;
startInfo. dwFlags         = STARTF_USESTDHANDLES | otherFlags;
startInfo. wShowWindow     = SW_SHOW;
startInfo. cbReserved2     = 0;
startInfo. lpReserved2     = 0;
startInfo. lpReserved      = 0;
startInfo. hStdInput       = 0;
startInfo. hStdOutput      = hApplicationOutputFile;
startInfo. hStdError       = hApplicationOutputFile;

std::string appPathWithArgs = m_GetFullPathOfTestApplication() + GetStartArgs();

CreateProcess (
    NULL,
    LPTSTR (appPathWithArgs. c_str()),
    NULL,
    NULL,
    TRUE, // handles are inherited
    0,
    NULL,
    NULL,
    &startInfo,
    &m_procInfo);

Nach Beendigung der Testapplikation werden die aufgezeichneten Ausgaben überprüft:

std::fstream outFile; // file with captured stdout and stderr of test app
outFile.open(nameApplicationOutputFile.c_str());
std::string line;
while( std::getline( outFile, line ) )
{
    // write captured output of test application to std out
    std::cout << line << std::endl;

    if (line.find("Detected memory leaks") !=string::npos)
    {
        exitCode = 1; // generate error
        std::cout << "****** MEM LEAK FOUND!!! ****" << std::endl;
    }
}
outFile.close();

Ein Beispiel für eine technische Realisierung einer steuernden Applikation findet sich in TestToolBox-TimedExec. Weitere Aufgaben einer derartigen steuernden Applikation sind in Abschnitt "Zeitbeschränkte Ausführung (TimedExec)" beschrieben.