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))) {
}
static void start() {
if (report_)
report_->onStart();
}
static void end() {
if (report_)
report_->onEnd();
}
static void startTest(const std::string& name) {
if (report_)
report_->onStartTest(name);
}
static void endTest() {
if (report_)
report_->onEndTest();
}
static void failure(const Notice& failure) {
if (report_)
report_->onFailure(failure);
if (FailFast::enabled())
throw AbortSuite();
}
static void eval(const Notice& eval) {
if (report_)
report_->onEval(eval);
}
static void info(const Notice& info) {
if (report_)
report_->onInfo(info);
}
static void warn(const Notice& warn) {
if (report_)
report_->onWarn(warn);
}
static void 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
...
struct class 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