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