====== 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 ===== ==== Header ==== 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 #include 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 #include 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 #include #include 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 {{:cpp:cppunit:stack_base.zip|ZIP-Datei zum Download}} bereit. Das Makefile erzeugt ohne Argumente das (fehlerhafte) Programm, mit ''make help'' wird das Testprogramm erzeugt.