risorse | good unit tests

Good Unit Tests /6

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.

Introduzione

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
 */

Sviluppi futuri

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:

Codice sorgente

Pagina modificata il 17/10/2012