risorse | good unit tests
Questa parte (la decima; qui la prima, qui la seconda, qui la terza, qui la quarta, qui la quinta, qui la sesta, qui la settima, qui l'ottava e qui la nona) è dedicata alla definizione di report polimorfici che non discendano dall'oggetto gut::Report (cfr. polimorfismo senza ereditarietà).
Senza la derivazione dalla classe base, la definizione del report di test diventa:
// test-gut.cpp ... class TestReport: public gut::Report{ std::string& failure_; std::string& eval_; std::string& info_; std::string& warn_; // disabled! TestReport(const TestReport&); TestReport& operator=(const TestReport&); public: TestReport( std::string& failure, std::string& eval, std::string& info, std::string& warn) : failure_(failure) , eval_(eval) , info_(info) , warn_(warn) { }protected:void onStart() { } void onEnd() { } void onStartTest(const std::string& /*name*/) { } void onEndTest() { }virtualvoid onFailure(const gut::Notice& failure) { failure_ = failure.what(); }virtualvoid onEval(const gut::Notice& eval) { eval_ = eval.what(); }virtualvoid onInfo(const gut::Notice& info) { info_ = info.what(); }virtualvoid onWarn(const gut::Notice& warn) { warn_ = warn.what(); } void onQuit() { } int failedTestCount() { return -1; } };
L'interfaccia non è più virtuale, ma implicita. In questo modo, qualunque oggetto può essere utilizzato come report, purché implementi i seguenti metodi:
// implicit Report interface void onStart(); void onEnd(); void onStartTest(const std::string& name); void onEndTest(); void onFailure(const Notice& failure); void onEval(const Notice& eval); void onInfo(const Notice& info); void onWarn(const Notice& warn); void onQuit(); int failedTestCount();
L'istanziazione del nuovo report di test cambia di conseguenza:
... int main() { std::string lastFailure; std::string lastEval; std::string lastInfo; std::string lastWarn;gut::Report::set(std::make_shared<TestReport>(lastFailure, lastEval, lastInfo, lastWarn));gut::Report::set(TestReport(lastFailure, lastEval, lastInfo, lastWarn)); ...
Report sta giocando tre ruoli distinti: definizione dell'interfaccia, contenitore del report concreto e riferimento centralizzato del reporting. Conviene separare le tre responsabilità: a Concept la definizione dell'interfaccia, a Report il contenimento, a theReport il punto d'accesso:
class Report {static std::shared_ptr<Report> report_;protected:Clock clock_;public:static void set(std::shared_ptr<Report> report) {report_ = report;template<class T> Report(T report) : report_ (std::make_shared<Model<T>>(std::move(report))) { }staticvoid start() {if (report_)report_->onStart(); }staticvoid end() {if (report_)report_->onEnd(); }staticvoid startTest(const std::string& name) {if (report_)report_->onStartTest(name); }staticvoid endTest() {if (report_)report_->onEndTest(); }staticvoid failure(const Notice& failure) {if (report_)report_->onFailure(failure); if (FailFast::enabled()) throw AbortSuite(); }staticvoid eval(const Notice& eval) {if (report_)report_->onEval(eval); }staticvoid info(const Notice& info) {if (report_)report_->onInfo(info); }staticvoid warn(const Notice& warn) {if (report_)report_->onWarn(warn); }staticvoid quit() {if (report_)report_->onQuit(); } int failedTestCount() { return report_->failedTestCount(); }protected:virtual void onStart() { }virtual void onEnd() { }virtual void onStartTest(const std::string& /*name*/) { }virtual void onEndTest() { }virtual void onFailure(const Notice& /*failure*/) { }virtual void onEval(const Notice& /*eval*/) { }virtual void onInfo(const Notice& /*info*/) { }virtual void onWarn(const Notice& /*warn*/) { }virtual void onQuit() { }private: struct Concept { virtual ~Concept() { } virtual void onStart() = 0; virtual void onEnd() = 0; virtual void onStartTest(const std::string& /*name*/) = 0; virtual void onEndTest() = 0; virtual void onFailure(const Notice& /*failure*/) = 0; virtual void onEval(const Notice& /*eval*/) = 0; virtual void onInfo(const Notice& /*info*/) = 0; virtual void onWarn(const Notice& /*warn*/) = 0; virtual void onQuit() = 0; virtual int failedTestCount() = 0; }; template<class T> struct Model : public Concept { T report_; Model(T report) : report_(std::move(report)) { } virtual void onStart() { report_.onStart(); } virtual void onEnd() { report_.onEnd(); } virtual void onStartTest(const std::string& name) { report_.onStartTest(name); } virtual void onEndTest() { report_.onEndTest(); } virtual void onFailure(const Notice& failure) { report_.onFailure(failure); } virtual void onEval(const Notice& eval) { report_.onEval(eval); } virtual void onInfo(const Notice& info) { report_.onInfo(info); } virtual void onWarn(const Notice& warn) { report_.onWarn(warn); } virtual void onQuit() { report_.onQuit(); } virtual int failedTestCount() { return report_.failedTestCount(); } }; std::shared_ptr<Concept> report_; };
L'accesso alle funzioni di reporting avviene attraverso una nuova interfaccia statica:
std::shared_ptr<Report> Report::report_;class theReport { static Report report_; public: static void set(Report report) { report_ = report; } static void start() { report_.start(); } static void end() { report_.end(); } static void startTest(const std::string& name) { report_.startTest(name); } static void endTest() { report_.endTest(); } static void failure(const Notice& failure) { report_.failure(failure); } static void eval(const Notice& eval) { report_.eval(eval); } static void info(const Notice& info) { report_.info(info); } static void warn(const Notice& warn) { report_.warn(warn); } static void quit() { report_.quit(); } static int failedTestCount() { return report_.failedTestCount(); } };
Il report di default non discende più da Report:
class DefaultReport: public Report{ size_t tests_;size_t testFailures_;bool testFailed_; size_t totalFailures_; size_t failedTests_; std::vector<Notice> log_; Clock clock_; public:size_tint failedTestCount() const { return static_cast<int>(failedTests_); }DefaultReport() : tests_(0), testFailures_(0), totalFailures_(0), failedTests_(0) { }protected:virtualvoid onStart() { tests_ = 0; failedTests_ = 0; std::cout << "Test suite started..." << std::endl; }virtualvoid onEnd() { std::cout << "Ran " << tests_ << " test(s) in " << clock_.elapsedTime() << "s." << std::endl; if (failedTests_ == 0) std::cout << color::lime << "OK - all tests passed." << color::reset << std::endl; else std::cout << color::red << "FAILED - " << totalFailures_ << " failure(s) in " << failedTests_ << " test(s)." << color::reset << std::endl; }virtualvoid onStartTest(const std::string& name) { ++tests_;testFailures_ = 0;testFailed_ = false; std::cout << name << ": "; }virtualvoid onEndTest() {if (testFailures_ == 0) {if (!testFailed_) { std::cout << "OK" << std::endl; flushLog(e_warning); } else testFailed(); clear(); }virtualvoid onFailure(const Notice& failure) {if (testFailures_ == 0)if (!testFailed_) std::cout << "FAILED" << std::endl; log_.push_back(failure);++testFailures_;++totalFailures_; testFailed_ = true; }virtualvoid onEval(const Notice& eval) { log_.push_back(eval); }virtualvoid onInfo(const Notice& info) { log_.push_back(info); }virtualvoid onWarn(const Notice& warn) { log_.push_back(warn); }virtualvoid onQuit() { testFailed(); } void testFailed() { ++failedTests_; flushLog(e_info); } void flushLog(Level minLevel) { for (auto notice : log_) if (notice.level() >= minLevel) std::cout << " " << notice.toString() << std::endl; } void clear() { log_.clear(); } }; Report theReport::report_ = Report(DefaultReport()); ... int runTests_() {auto report_ = std::make_shared<gut::DefaultReport>();gut::Report::set(report_);...return report_->failedTestCount();return gut::Report::failedTestCount(); }
Una sostituzione globale delle chiamate gut::Report in gut::theReport nei file gut.h e test-gut.cpp conclude l'intervento.
Come esempio di report alternativo ho provato ad implementare TAP[1][2]:
// file gut-tap.h #ifndef GUT_TAP_H #define GUT_TAP_H namespace gut { class TapReport { int tests_; int failedTests_; std::string testName_; bool testFailed_; std::vector<gut::Notice> log_; bool quit_; public: int failedTestCount() const { return failedTests_; } void onStart() { quit_ = false; tests_ = 0; failedTests_ = 0; } void onEnd() { if (quit_) return; std::cout << "1.." << tests_ << std::endl; if (tests_ > 0) { float okRatio = (tests_ - failedTests_) * 100. / tests_; std::cout << "# failed " << failedTests_ << "/" << tests_ << " test(s), " << std::fixed << std::setprecision(1) << okRatio << "% ok" << std::endl; } } void onStartTest(const std::string& name) { ++tests_; testName_ = name; testFailed_ = false; } void onEndTest() { std::ostringstream oss; if (testFailed_) { ++failedTests_; oss << "not "; } oss << "ok " << tests_ << " - " << testName_; for (const auto& entry : log_) { oss << "\n# " << entry.toString(); } std::cout << oss.str() << std::endl; log_.clear(); } void onFailure(const gut::Notice& failure) { log_.push_back(failure); testFailed_ = true; } void onEval(const gut::Notice& eval) { log_.push_back(eval); } void onInfo(const gut::Notice& info) { log_.push_back(info); } void onWarn(const gut::Notice& warn) { log_.push_back(warn); } void onQuit() { quit_ = true; std::ostringstream oss; oss << "Bail out!"; std::cout << oss.str() << std::endl; } }; } // namespace gut #endif // GUT_TAP_H
Per attivare il report prescelto si sfrutta il costruttore di un oggetto statico:
// file gut.h ... #define GUT_ENABLE_REPORT(name_) \ static struct CustomReport { \ CustomReport() { gut::theReport::set(name_()); } \ } customReport_; ... // file example.cpp ... GUT_ENABLE_REPORT(gut::TapReport) ... /* output: * * ok 1 - Initial list is empty * ok 2 - Insertion to empty list is retained * ok 3 - Distinct insertions are retained in stack order * ok 4 - Duplicate insertions are moved to the front but not inserted * ok 5 - Out of range indexing throws exception * 1..5 * # failed 0/5 test(s), 100.0% ok */
Per supportare al meglio il protocollo TAP è necessario estendere il framework di test.
TAP usa la direttiva todo per segnalare test incompleti, per quali è quindi previsto il fallimento. Un todo-test che non fallisce è candidato ad essere promosso al rango di test. Un modo per ottenere questo tipo di segnalazione consiste nel decorare il nome del test con la descrizione che accompagna la direttiva:
// gut-tap.h ... #define TODO(name_, desc_) \ TEST(name_ " # todo " desc_) // example.cpp ... TODO("Empty strings are ignored", "not yet implemented") { RecentlyUsedList aList; aList.insert(""); CHECK(aList.empty()); } /* output: * * ... * not ok 6 - Empty strings are ignored # todo not yet implemented * # example.cpp(85) : [error] aList.empty() evaluates to false * 1..6 * # failed 1/6 test(s), 83.3% ok */
La direttiva skip caratterizza i test che per qualche ragione non sono stati eseguiti; i test ignorati devono essere trattati alla stessa stregua dei test conclusi con successo. A tal scopo è stata definita la macro SKIP. Essa introduce un test vuoto, destinato quindi al successo, inibendo nel contempo il test originale definendo una funzione ausiliaria di comodo, non statica – per evitare l'emissione di messaggi d'errore da parte del linker –, il cui scopo è di “agganciare” il codice di test e renderlo inoperativo:
SKIP(name_, reason_) { TEST(name_ " # skipped, reason: " reason_) { <test body> // empty tesy } } void skip_ { <test body> }
L'implementazione è la seguente:
// gut-tap.h ... #define SKIP(name_, reason_) \ TEST(name_ " # skipped, reason: " reason_) { \ } \ void skip ## __LINE__() // example.cpp ... SKIP("One trillion insertions are ok", "too slow!") { RecentlyUsedList aList; for (int i = 0; i < 1000000; ++i) for (int j = 0; j < 1000000; ++j) for (int k = 0; k < 1000000; ++k) aList.insert("one"); CHECK(aList.size() == 1); CHECK(aList[0] == "one"); } /* output: * * ... * ok 7 - One trillion insertions are ok # skipped, reason: too slow! * 1..7 * # failed 1/7 test(s), 85.7% ok */
Bail out! è un'indicacazione d'uscita anticipata ed inattesa dal test; l'eccezione AbortSuite può essere utilizzata allo scopo:
// gut-tap.h ... #define BAIL_OUT \ do { \ throw gut::AbortSuite(); \ } while (0) // example.cpp ... TEST("Write to a file") { BAIL_OUT; } /* output: * * ... * ok 7 - One trillion insertions are ok # skipped, reason: too slow! * Bail out! */
Per chiarezza, conviene corredare l'evento di una descrizione che ne dettagli la natura:
TEST("Write to a file") { BAIL_OUT("File system is read only."); }
Con pochi interventi di aggiornamento si ottiene quando desiderato:
// gut-tap.h ... class TapReport { ... void onQuit(const std::string& reason) { quit_ = true; std::ostringstream oss; oss << "Bail out!"; if (!reason.empty ()) oss << " Reason: " << reason; std::cout << oss.str() << std::endl; } ... #define BAIL_OUT(reason_) \ do { \ throw gut::AbortSuite(reason_); \ } while (0) // gut.h ...structclass AbortSuite { std::string reason_; public: AbortSuite() { } AbortSuite(const std::string& reason) : reason_(reason) { } const std::string& reason() const { return reason_; } }; ... class Report { ... void quit(const std::string& reason) { report_->onQuit(reason); } ... private: struct Concept { ... virtual void onQuit(const std::string& /*reason*/) = 0; ... struct Model : public Concept { ... virtual void onQuit(const std::string& reason) { report_.onQuit(reason); } ... }; ... class theReport { ... static void quit(const std::string& report) { report_.quit(report); } ... }; ... class DefaultReport { ... void onQuit(const std::string& /*reason*/) { testFailed(); } ... }; ... int runTests_() { ... } catch(const gut::AbortSuite& e) { gut::theReport::quit(e.reason()); ... }
L'esecuzione del test d'esempio genera ora un messaggio più dettagliato:
/* output: * * ... * ok 7 - One trillion insertions are ok # skipped, reason: too slow! * Bail out! Reason: File system is read only. */
Pagina modificata il 04/02/2014