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:
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:
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:
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:
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:
Teilen durch 0:
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.
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):
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:
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.