Testimplementation

Ein Unit-Test besteht normalerweise nicht aus einer einzelnen Überprüfung, sondern aus einer Vielzahl von Szenarien, die abgetestet werden. Hierfür wird üblicherweise eine Test-Klasse geschrieben, die eine Reihe von Funktionen bietet.

Grundfunktionalität

Zum Einstieg testen wir Grundfunktionalität ab. Hierfür schreiben wir zunächst die Testklasse, die wir von der Klasse CppUnit::TextFixture ableiten. Ein Fixture ist ein „Stammkunde“, also etwas, was in diesem Zusammenhang immer wieder getestet wird.

#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
 
class TestStack : public CppUnit::TestFixture
{
  CPPUNIT_TEST_SUITE( TestStack );
 
  CPPUNIT_TEST( TestStackSize );
  CPPUNIT_TEST( TestStackPush );
  CPPUNIT_TEST( TestStackPop );
  CPPUNIT_TEST( TestStackBuffer );
 
  CPPUNIT_TEST_SUITE_END();
 
 public:
  void TestStackSize(void);
  void TestStackPush(void);
  void TestStackPop(void);
  void TestStackBuffer(void);
};

Die Ableitung ist notwendig, damit CppUnit, diese Testsuite in die eigenen Listen aufnehmen kann. Eine Testsuite ist eine Sammlung von Tests.

Makros innerhalb der Klasse beschreiben zunächst, dass es sich um eine Testsuite handelt. CPPUNIT_TEST_SUITE meldet diese an und bekommt als Argument den Namen der Testklasse. Anschließend werden die einzelnen durchzuführenden Methodennamen der Tests einzeln mit CPPUNIT_TEST aufgelistet und mit dem Makro CPPUNIT_TEST_SUITE_END abgeschlossen.

Anschließend werden die eigentlich Methoden für C++ aufgeführt. Hiermit haben wir den Header soweit abgeschlossen.

Implementierung

Schauen wir uns nun eine Implementierung an:

#include "teststack.h"
#include "stack.h"
 
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
 
CPPUNIT_TEST_SUITE_REGISTRATION( TestStack );

Mit dem Makro CPPUNIT_TEST_SUITE_REGISTRATION wird die Testsuite TestStack bei CppUnit registriert. Alle registrierten Testsuites werden im Testprogramm getestet.

Schauen wir uns an, wie ein Test grundsätzlich aufgebaut wird:

void TestStack::TestStackSize(void)
{
  Stack stack( 10 );
 
  CPPUNIT_ASSERT_EQUAL( 10u, stack.GetSize() );
  CPPUNIT_ASSERT_EQUAL(  0u, stack.GetUsed() );
}

Ein Test bereitet die das zu testende Objekt vor und bringt es in den Zustand, in dem es getestet werden soll. In diesem Fall prüfen wir, ob der Konstruktor die Größe und die bisher genutzten Elemente korrekt initialisiert.

Das Makro CPPUNIT_ASSERT_EQUAL bekommt zwei Argumente, zum ersten den erwarteten Wert (hier also der Wert 10 als unsigned int) und als zweites den im Test ermittelten Wert.

Entsprechend werden die anderen Grundlagen-Tests implementiert:

void TestStack::TestStackPush(void)
{
  Stack stack( 10 );
 
  CPPUNIT_ASSERT_EQUAL( 0u,   stack.GetUsed() );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 1 ) );
  CPPUNIT_ASSERT_EQUAL( 1u,   stack.GetUsed() );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 2 ) );
  CPPUNIT_ASSERT_EQUAL( 2u,   stack.GetUsed() );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 3 ) );
  CPPUNIT_ASSERT_EQUAL( 3u,   stack.GetUsed() );
}
 
void TestStack::TestStackPop(void)
{
  Stack stack( 10 );
 
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 1 ) );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 2 ) );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 3 ) );
 
  unsigned int value;
  CPPUNIT_ASSERT_EQUAL( 3u,  stack.GetUsed() );
  CPPUNIT_ASSERT_EQUAL( true, stack.Pop( value ) );
  CPPUNIT_ASSERT_EQUAL( 3u,  value );
 
  CPPUNIT_ASSERT_EQUAL( 2u,  stack.GetUsed() );
  CPPUNIT_ASSERT_EQUAL( true, stack.Pop( value ) );
  CPPUNIT_ASSERT_EQUAL( 2u,  value );
 
  CPPUNIT_ASSERT_EQUAL( 1u,  stack.GetUsed() );
  CPPUNIT_ASSERT_EQUAL( true, stack.Pop( value ) );
  CPPUNIT_ASSERT_EQUAL( 1u,  value );
  CPPUNIT_ASSERT_EQUAL( 0u,  stack.GetUsed() );
}
 
void TestStack::TestStackBuffer(void)
{
  Stack stack( 10 );
 
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 1 ) );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 2 ) );
  CPPUNIT_ASSERT_EQUAL( true, stack.Push( 3 ) );
 
  CPPUNIT_ASSERT_EQUAL( 1u, stack.GetBuffer()[ 0 ] );
  CPPUNIT_ASSERT_EQUAL( 2u, stack.GetBuffer()[ 1 ] );
  CPPUNIT_ASSERT_EQUAL( 3u, stack.GetBuffer()[ 2 ] );
}

Das Testprogramm

Neben dem Hauptprogramm wird nun ein Programm geschrieben, das die registrierten Tests aufruft. Der Testrunner, erhält die registrierten Test und einen „Outputter“, der die Testausgaben aufbereitet. In der Regel ist das die Konsole, aber zur weiteren Verwendung in anderen kann auch der XmlOutputter helfen, der die Testergebnisse im XML-Format ausgibt. Für unser Testprogramm bewegen wir uns jedoch auf der Konsole:

#include "stack.h"
#include "teststack.h"
 
#include <cppunit/CompilerOutputter.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
 
int main(void)
{
  /* Test-Runner erzeugen */
  CppUnit::TextTestRunner runner;
 
  /* TestSuite erzeugen */
  CppUnit::Test *suite = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
 
  runner.addTest( suite );
 
  /* Outputformatierung festlegen */
  runner.setOutputter( new CppUnit::CompilerOutputter( &runner.result(), std::cerr ) );
 
  /* Tests durchführen 
  ** und 0 oder im Fehlerfall 1 zurückmelden */
  return runner.run() ? 0 : 1;
}

Schauen wir uns nun die Ausgaben des Programms an:

# ./test 
....

OK (4)

Die abgetesteten Grundlagen entsprechen also den Erwartungen.

Was sagen Tests aus?

Und hier haben wir eine wichtige Lektion, die man sich bitte bewusst macht: Der Stack ist nun getestet, aber wir wissen ja bereits, dass der Stack nicht richtig funktioniert. Tests sichern nur die Funktion ab, die sie abtesten. So sinnvoll es ist, Funktionen abzutesten, um die Qualität der Software zu verbessern - man kann trotz guter Tests nur schwer garantieren, dass man alles abgetestet hat, was fehlschlagen kann.

Tests sagen also nicht aus, dass etwas funktioniert, sie erhöhen aber die Wahrscheinlichkeit, weniger Fehler zu produzieren. Insbesondere wenn man getestete Programme überarbeitet, helfen Tests dabei, ungewollte Änderungen anzuzeigen, wenn die getestete Klasse nicht mehr die Ergebnisse produziert, die man in den Tests erwartet.

Download

Die Grundlagentests stehen als ZIP-Datei zum Download bereit. Das Makefile erzeugt ohne Argumente das (fehlerhafte) Programm, mit make help wird das Testprogramm erzeugt.