Unit testy v C++
Každý program, který programátor vyvíjí je nutné správně testovat. Již nějakou dobou se objevují termíny jako test-driven development. Jedná se o přístup, kdy programátor kromě zdrojového kódu aplikace vyvíjí a udržuje také zdrojový kód automatických testů. Tyto testy se skládají z jednotlivých funkcí, kdy každá funkce testuje jednu jednotku cílového kódu. Z tohoto důvodu se tyto testy nazývají unit testy (jednotkové testy).
Optimální postup by měl vypadat tak, že nejdříve bude navržen soubor testů, které jednoznačně a zcela popíšou testovanou funkci. Tyto testy budou implementovány a otestovány. Následně proběhne implementace vlastního algoritmu funkce. Tento přístup je ale velmi často ohýbán a převracen, především s cílem urychlení vývoje v jeho počátku.
Téměř pro každý programovací jazyk existuje soubor knihoven, které lze využít pro účely jednotkového testování. Jednou z knihoven pro C++ je soubor knihoven cppunit. Framework je možné nainstalovat ze zdrojových kódů, případně z repozitáře používané distribuce.
1 |
sudo apt-get install libcppunit-dev |
Příklad na kterém bude předveden princip jednotkových testů bude předveden na třídě poskytující rozhraní pro základní matematické operace (sčítání a odečítání). Celý projekt je možné stáhnout zde.
Projekt obsahuje základní třídu Base, která navrhuje dvě metody – plus(int a, int b) a minus(int a, int b).
1 2 3 4 5 6 7 8 9 10 11 |
#ifndef _H_BASE #define _H_BASE class Base { public: Base(); int plus(int a, int b); int minus(int a, int b); }; #endif |
Jejich implementace je prostá, ale pro přehlednost je uložena v cpp souboru. Tato vlastní implementace není důležitá a pro účely článku je nezajímavá. Implementace testů je v podsložce units, takovéto dělení je vhodné zejména pro udržení přehlednosti ve zdrojovém kódu.
Pro účely vytvoření základního jednotkového testu je nutné implementovat hlavní funkci main(). V těle této metody bude provedena registrace jednotlivých dílčích testů. Nakonec jsou všechny zaregistrované testy provedeny.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> #include <cppunit/TestSuite.h> #include <cppunit/ui/text/TestRunner.h> #include "base_test.hpp" using namespace std; int main() { CppUnit::TextUi::TestRunner runner; cout << "unit tests for BaseClass" << endl; runner.addTest(BaseTestCase::suite()); cout << "Running the unit tests." << endl; runner.run(); return 0; } |
Jak je ze zdrojového kódu možné vyčíst, nejprve je vytvořena instance objektu TestRunner. V tomto objektu jsou registrovány jednotlivé jednotkové testy. Tyto testy jsou registrovány prostřednictvím zavolání statické metody BaseTestCase::suite().
Základní metoda testovací třídy je BaseTestCase, třída je potomkem třídy typu CppUnit::TestFixture.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#ifndef _H_BASE_TEST_CASE #define _H_BASE_TEST_CASE #include <iostream> #include <cppunit/TestFixture.h> #include <cppunit/TestAssert.h> #include <cppunit/TestCaller.h> #include <cppunit/TestSuite.h> #include <cppunit/TestCase.h> #include "base.hpp" class BaseTestCase : public CppUnit::TestFixture { public: void setUp(); void tearDown(); static CppUnit::Test *suite() { CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite("BaseTestClass"); // Register all test functions suiteOfTests->addTest( new CppUnit::TestCaller<BaseTestCase>("Test1 - Unique Solution.", &BaseTestCase::loadTest)); suiteOfTests->addTest( new CppUnit::TestCaller<BaseTestCase>("Test1 - Unique Solution.", &BaseTestCase::testMinusFunction)); return suiteOfTests; } protected: void loadTest(); void testMinusFunction(); void storeTest(); private: Base *baseClass; }; #endif |
CppUnit::TestFixture definuje metody void setUp() a void tearDown(). První metoda je zavolána vždy před zavoláním metody testu. Obsahem této metody by měla být inicializace všech proměnných, které jsou nutné pro provedení testů.
Následně je zavolána metoda testu, která provede vlatní test. Po provedení obsahu testovací metody je zavolána metoda tearDown, která provede vyčištění (vymazání) všech dynamicky alokovaných proměnných. Metoda tearDown je zavolána i v případě, že je prováděný test neúspěšný.
V následujícím bloku zdrojového kódu je uvedena implementace základních testů.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> #include "base_test.hpp" using namespace std; void BaseTestCase::setUp() { baseClass = new Base(); } void BaseTestCase::tearDown() { delete baseClass; baseClass = NULL; } void BaseTestCase::loadTest() { cout << "Load tests" << endl; if(baseClass->plus(1, 1) == 2) { cout << "Test OK" << endl; } if(baseClass->plus(2,2) == 5) { cout << "Test FAILED" << endl; } } void BaseTestCase::testMinusFunction() { cout << "First test minus" << endl; CPPUNIT_ASSERT(baseClass->minus(10, 2) == 8); } void BaseTestCase::storeTest() { cout << "Store test results" << endl; } |
Z implementace základních testů je možné získat pohled na to, jak lze provést test na to, zda je výsledek testu správny (1 + 1 = 2), nebo zda je výsledek testu nesprávný, což může být i správná situace (2 + 2 != 5). Při psaní unit testů, které jsou součástí velkých projektů je často využita kombinace obou těchto přístupů.