risorse | good unit tests
Questa parte (la sesta; qui la prima, qui la seconda, qui la terza, qui la quarta e qui la quinta) mostra una possibile strutturazione del codice di test in un insieme di TEST che costituiscono una TEST_SUITE.
L'implementazione si ispira fortemente a quanto presentato da Henney: è facilmente individuabile la corrispondenza TEST_SUITE ↔ SPECIFICATION, TEST ↔ PROPOSITION – il concetto di DIVISION, che costituisce un'ulteriore suddivisione gerarchica di PROPOSITION, l'ho tralasciato di proposito. I concetti chiave sono due:
TEST_SUITE { → void runTests(...) { TEST("1st test") { → if (...) { // test code // test code } } TEST("2nd test") { → if (...) { // test code // test code } } // ... // ... } }
L'idea originale di Henney consiste nell'introdurre un oggetto di comodo, che lui chiama execution, che vien passato alla TEST_SUITE (ovvero alla funzione runTest), e al quale ogni TEST chiede l'autorizzazione per l'esecuzione:
void runTests(execution& e) { if (e.shouldRun("1st test")) { // test code } if (e.shouldRun("2nd test")) { // test code } // ... }
La scelta di strutturare i singoli test in una successione di if permette di condividere facilmente strutture ausiliarie necessarie a più test:
void runTests(execution& e) { TestData someTestData; if (e.shouldRun("1st test")) {TestData someTestData;// test code } if (e.shouldRun("2nd test")) {TestData someTestData;// test code } // ... }
Per garantire l'esecuzione dei vari test in isolamento, si effettua una prima chiamata a runTests con un execution che raccoglie il nome dei test presenti, senza attivarne nessuno; i singoli test sono quindi eseguiti sequenzialmente, con una chiamata runTests dedicata, sfruttando un ulteriore execution che attiva il solo test prescelto:
struct TestSelection { virtual bool shouldRun(const std::string& testName) = 0; }; struct Schedule : public TestSelection { std::vector<std::string> testNames; size_t size() const { return testNames.size(); } virtual bool shouldRun(const std::string& testName) { testNames.push_back(testName); return false; } }; struct SingleTestSelection : public TestSelection { std::string testName_; SingleTestSelection(const std::string& testName) : testName_(testName) { } virtual bool shouldRun(const std::string& testName) { return testName == testName_; } }; // ... int main() { // ... // retrieve test names gut::Schedule schedule; runTests(schedule); // execute the tests one by one for (const std::string& testName : schedule.testNames) { gut::SingleTestSelection testsToPerform(testName); // ... runTests(testsToPerform); // ... } // ... }
Con l'occasione è stata arricchita l'interfaccia Report:
class Report { // ... public: static void start(const std::string& label) { if (report_) report_->onStart(label); } // ... static void startTest(const std::string& name) { if (report_) report_->onStartTest(name); } static void endTest() { if (report_) report_->onEndTest(); } // ... protected: virtual void onStart(const std::string& /*label*/) { } // ... virtual void onStartTest(const std::string& /*name*/) { } virtual void onEndTest() { } // ... };
L'oggetto DefaultReport sfrutta la nuova interfaccia per fornire indicazioni più precise circa lo svolgimento del test:
class DefaultReport : public gut::Report { size_t tests_; size_t testFailures_; size_t totalFailures_; size_t failedTests_; public: size_t failedTestCount() const { return failedTests_; } DefaultReport() : tests_(0), testFailures_(0), totalFailures_(0), failedTests_(0) { } protected: virtual void onStart(const std::string& label) { std::cout << "Testing " << label << "..." << std::endl; } virtual void onEnd() { std::cout << "Ran " << tests_ << " test(s) in " << clock_.elapsedTime() << "s." << std::endl; if (failedTests_ == 0) std::cout << "OK - all tests passed." << std::endl; else std::cout << "FAILED - " << totalFailures_ << " failure(s) in " << failedTests_ << " test(s)" << std::endl; } virtual void onStartTest(const std::string& name) { ++tests_; testFailures_ = 0; std::cout << name << ": "; } virtual void onEndTest() { if (testFailures_ == 0) std::cout << "OK" << std::endl; else ++failedTests_; } virtual void onFailure(const gut::Failure& failure) { if (testFailures_ == 0) std::cout << "FAILED" << std::endl; std::cout << " " << failure.location.file << "(" << failure.location.line << ") : " << failure.what() << std::endl; ++testFailures_; ++totalFailures_; } };
Segue la definizione completa delle macro TEST_SUITE e TEST:
#define TEST_SUITE(name_) \ void runTests_(gut::TestSelection& selection_); \ int main() { \ gut::Schedule schedule_; \ runTests_(schedule_); \ auto report_ = std::make_shared<gut::DefaultReport>(); \ gut::Report::set(report_); \ gut::Report::start(name_); \ for (const std::string& testName_ : schedule_.testNames) { \ gut::SingleTestSelection testsToPerform_(testName_); \ gut::Report::startTest(testName_); \ try { \ runTests_(testsToPerform_); \ } catch(const gut::AbortTest&) { \ } catch(const std::exception& e_) { \ gut::Report::failure(gut::UnexpectedExceptionFailure(e_, __FILE__, __LINE__)); \ } catch(...) { \ gut::Report::failure(gut::UnknownExceptionFailure(__FILE__, __LINE__)); \ } \ gut::Report::endTest(); \ } \ gut::Report::end(); \ return report_->failedTestCount(); \ } \ void runTests_(gut::TestSelection& selection_) #define TEST(name_) \ if (selection_.shouldRun(name_))
Infine, la nuova versione del progamma di test della classe RecentlyUsedList:
TEST_SUITE("RecentlyUsedList") { TEST("Initial list is empty") { RecentlyUsedList anEmptyList; CHECK(anEmptyList.empty()); CHECK(anEmptyList.size() == 0); } TEST("Insertion to empty list is retained") { RecentlyUsedList aListWithOneElement; aListWithOneElement.insert("one"); CHECK(!aListWithOneElement.empty()); CHECK(aListWithOneElement.size() == 1); CHECK(aListWithOneElement[0] == "one"); } TEST("Distinct insertions are retained in stack order") { RecentlyUsedList aListWithManyElements; aListWithManyElements.insert("one"); aListWithManyElements.insert("two"); aListWithManyElements.insert("three"); CHECK(!aListWithManyElements.empty()); CHECK(aListWithManyElements.size() == 3); CHECK(aListWithManyElements[0] == "three"); REQUIRE(aListWithManyElements[1] == "two"); CHECK(aListWithManyElements[2] == "one"); } TEST("Duplicate insertions are moved to the front but not inserted") { RecentlyUsedList aListWithDuplicatedElements; aListWithDuplicatedElements.insert("one"); aListWithDuplicatedElements.insert("two"); aListWithDuplicatedElements.insert("three"); aListWithDuplicatedElements.insert("two"); CHECK(!aListWithDuplicatedElements.empty()); CHECK(aListWithDuplicatedElements.size() == 3); CHECK(aListWithDuplicatedElements[0] == "two"); CHECK(aListWithDuplicatedElements[1] == "three"); CHECK(aListWithDuplicatedElements[2] == "one"); } TEST("Out of range indexing throws exception") { RecentlyUsedList aListWithOneElement; aListWithOneElement.insert("one"); THROWS(aListWithOneElement[1], std::out_of_range); } } /* output: * Testing RecentlyUsedList... * Initial list is empty: OK * Insertion to empty list is retained: OK * Distinct insertions are retained in stack order: OK * Duplicate insertions are moved to the front but not inserted: OK * Out of range indexing throws exception: OK * Ran 5 tests in 0.015s * OK - all tests passed */
La presenza della classe Schedule fa immediatamente pensare alla possibilità di selezionare i test da effettuare (o da ignorare) sulla base del nome, sfruttando magari i caratteri jolly oppure le espressioni regolari. Altre direzioni di sviluppo contemplano:
Pagina modificata il 17/10/2012