risorse | good unit tests

Good Unit Tests /14

Questa parte (la quattordicesima; qui la prima, qui la seconda, qui la terza, qui la quarta, qui la quinta, qui la sesta, qui la settima, qui l'ottava, qui la nona, qui la decima, qui l'undicesima, qui la dodicesima e qui la tredicesima) conclude il refactoring della classe Report, svincolando la definizione dei Custom Report dalla libreria.

Introduzione

Il type-erasure presentato nella decima parte permette di definire oggetti Report proprietari senza che questi debbano implementare un'interfaccia esplicita, cioé derivare da un oggetto di libreria. La dipendenza tuttavia sussiste ancora, a causa dei parametri dei metodi di notifica, tipicamente oggetti gut::Notice. Si vuole ora rimuovere definitivamente questa dipendenza superflua; nel contempo sposteremo in Report il conteggio dei test passati e falliti, attualmente implementata sia in DefaultReport che in TapReport.

La nuova interfaccia Report

La nuova interfaccia degli oggetti Report usa esclusivamente tipi primitivi o della libreria standard:

  virtual void onStart();
  virtual void start();

  virtual void onEnd();
  virtual void end(int tests, int failedTests, int failures, double duration);

  virtual void onStartTest(const std::string& name);
  virtual void startTest(const std::string& name);

  virtual void onEndTest();
  virtual void endTest(bool failed, double duration);

  virtual void onFailure(const Notice& failure);
  virtual void failure(const char* file, int line, int level, const std::string& what);

  virtual void onEval(const Notice& eval);
  virtual void onInfo(const Notice& info);
  virtual void onWarn(const Notice& warn);
  virtual void info(const char* file, int line, int level, const std::string& what);

  virtual void onQuit(const std::string& reason);
  virtual void quit(const std::string& reason);

  virtual int failedTestCount();

A parte la rimozione del prefisso on, i metodi onEval, onInfo e onWarn collassano su info, mentre failedTestCount decade, non essendo il conteggio dei test falliti responsabilità dell'oggetto proprietario.

Poiché l'oggetto theReport non si occupa di generare alcun prospetto, limitandosi ad inoltrare le notifiche all'implementazione concreta, per chiarezza è stato rinominato in theListener, e di conseguenza anche il suo tipo:

class Notice {
  // ...

public:
  Notice(Level level, const char* file, int line) : level_(level), location_(file, line) {
    content_ << "[" << level_name[level] << "] ";
  }
  Notice(const Notice& notice) : level_(notice.level_), location_(notice.location_) {
    content_ << notice.content_.str();
  }
  Location location() const {
    return location_;
  }

  std::string toString() const {
    std::ostringstream ss;
    ss << location_.file << "(" << location_.line << ") : " << what();
    return ss.str();
  }
  // ...

};

// ...

class Report Listener {
  size_t testCount_;
  int failedTestCount_;
  size_t totalFailureCount_;
  bool didTestFail_;
  Timer testTimer_;
  Timer globalTimer_;
public:
  template<class T>
  Report Listener(T report) : report_(std::make_shared<Model<T>>(std::move(report))) { }
  int failedTestCount() const { return failedTestCount_; }
  void start() { report_->onStart(); }
    testCount_ = 0;
    failedTestCount_ = 0;
    totalFailureCount_ = 0;
    globalTimer_.reset();
    report_->start();
  }
  void end() { report_->onEnd(); }
    report_->end(
      testCount_,
      failedTestCount_,
      totalFailureCount_,
      globalTimer_.elapsedTime());
    }
  void startTest(const std::string& name) { report_->onStartTest(name); }
    ++testCount_;
    didTestFail_ = false;
    testTimer_.reset();
    report_->startTest(name);
  }
  void endTest() { report_->onEndTest(); }
    if (didTestFail_)
      ++failedTestCount_;
    report_->endTest(didTestFail_, globalTimer_.elapsedTime());
  }
  void failure(const Notice& failure) {
    didTestFail_ = true;
    ++totalFailureCount_;
    report_->onFailure(failure);
    report_->failure(
      failure.location().file,
      failure.location().line,
      failure.level(),
      failure.what());
    if (FailFast::enabled())
      throw AbortSuite();
  }
  void eval(const Notice& eval) { report_->onEval(eval); }
  void info(const Notice& info) { report_->onInfo(info); }
    report_->info(
      info.location().file,
      info.location().line,
      info.level(),
      info.what());
  }
  void warn(const Notice& warn) { report_->onWarn(warn); }
  void quit(const std::string& reason) { report_->onQuit(reason); }
    report_->quit(reason);
  }
  int failedTestCount() { return report_->failedTestCount(); }
  // ...

Listener, oltre ad inoltrare le notifiche all'oggetto report_ concreto, aggiorna i contatori dei test e delle asserzioni fallite, e misura la durata dei singoli test, oltre a quella dell'intera suite. Le classi Concept e Model sono conformi alla nuova interfaccia:

class Listener {
// ...

private:
  struct Concept {
    virtual ~Concept() { }
    virtual void onStart() = 0;
    virtual void onEnd() = 0;
    virtual void onStartTest(const std::string&) = 0;
    virtual void onEndTest() = 0;
    virtual void onFailure(const Notice&) = 0;
    virtual void onEval(const Notice&) = 0;
    virtual void onInfo(const Notice&) = 0;
    virtual void onWarn(const Notice&) = 0;
    virtual void onQuit(const std::string&) = 0;
    virtual int failedTestCount() = 0;

    virtual void start() = 0;
    virtual void end(int, int, int, double) = 0;
    virtual void startTest(const std::string&) = 0;
    virtual void endTest(bool, double) = 0;
    virtual void failure(const char*, int, int, const std::string&) = 0;
    virtual void info(const char*, int, int, const std::string&) = 0;
    virtual void quit(const std::string&) = 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(const std::string& reason) { report_.onQuit(reason); }
    virtual int failedTestCount() { return report_.failedTestCount(); }

    virtual void start() {
      report_.start();
    }
    virtual void end(int tests, int failedTests, int failures, double duration) {
      report_.end(tests, failedTests, failures, duration);
    }
    virtual void startTest(const std::string& name) {
      report_.startTest(name);
    }
    virtual void endTest(bool failed, double duration) {
      report_.endTest(failed, duration);
    }
    virtual void failure(const char* file, int line, int level, const std::string& what) {
      report_.failure(file, line, level, what);
    }
    virtual void info(const char* file, int line, int level, const std::string& what) {
      report_.info(file, line, level, what);
    }
    virtual void quit(const std::string& reason) {
      report_.quit(reason);
    }
  };

  std::shared_ptr<Concept> report_;
};

Essendo impiegato per misurare la durata di ogni singolo test, l'oggetto Clock è stato arricchito del metodo reset e rinominato in Timer, nome più consono alla funzione ricoperta:

#ifdef GUT_HAS_CHRONO
struct Clock {
class Timer {
  std::chrono::steady_clock::time_point start_;
public:
  Clock() : start_(std::chrono::steady_clock::now()) { }
  Timer() { reset (); }
  void reset() { start_ = std::chrono::steady_clock::now(); }
  double elapsedTime() {
    return std::chrono::duration_cast<std::chrono::milliseconds>(
      std::chrono::steady_clock::now() - start_).count() / 1000.;
  }
};
#else
struct Clock {
class Timer {
  clock_t start_;
public:
  Clock() { start_ = clock(); }
  Timer() { reset(); }
  void reset() { start_ = clock(); }
  double elapsedTime() {
    return static_cast<double>(clock() - start_) / CLOCKS_PER_SEC;
  }
};
#endif

L'interfaccia statica theReport non è indispensabile, così viene sostituita da un più semplice oggetto statico di tipo Listener. I riferimenti a gut::theReport:: cambiano conseguentemente in gut::theListener., le chiamate eval e warn si trasformano in info:

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(const std::string& report) { report_.quit(report); }
  static int failedTestCount() { return report_.failedTestCount(); }
};

// ...

Report theReport::report_ = Report(DefaultReport());
Listener theListener = Listener(DefaultReport());

Cambia anche il codice di sostituzione dell'oggetto Report:

#define GUT_ENABLE_REPORT(name_) \
  static struct CustomReport { \
    CustomReport() { gut::theReport::set(name_()); } \
  } customReport_;

#define GUT_CUSTOM_REPORT(report_) \
  static struct ListenerWithCustomReport { \
    ListenerWithCustomReport() { \
      gut::theListener = gut::Listener(report_); \
    } \
  } aCustomListener_;

L'oggetto DefaultReport si snellisce, dovendosi ora occupare solo della creazione del prospetto:

class DefaultReport {
  size_t tests_;
  bool testFailed_;
  size_t totalFailures_;
  size_t failedTests_;
  std::vector<Notice> log_;
  Clock clock_;
  std::ostream& os_;
  bool testAlreadyFailed_;
  std::vector<std::pair<int, std::string>> log_;
public:
  int failedTestCount() const {
    return static_cast<int>(failedTests_);
  }
  DefaultReport(std::ostream& os = std::cout) : os_(os) { }
  void onStart() {
  void start() {
    tests_ = 0;
    failedTests_ = 0;
    std::cout os_ << "Test suite started..." << std::endl;
  }
  void onEnd() {
  void end(int tests, int failedTests, int failures, double duration) {
    std::cout os_ << "Ran " << tests_ tests << " test(s) in "
      << clock_.elapsedTime() << std::fixed << std::setprecision(0)
      << duration * 1000. << " ms." << std::endl;
    if (failedTests_ failedTests == 0)
      std::cout os_ << color::lime << "OK - all tests passed."
        << color::reset << std::endl;
    else
      std::cout os_ << color::red << "FAILED - " << totalFailures_ failures
        << " failure(s) in " << failedTests_failedTests << " test(s)."
        << color::reset << std::endl;
  }
  void onStartTest(const std::string& name) {
  void startTest(const std::string& name) {
    ++tests_;
    testFailed_ = false;
    testAlreadyFailed_ = false;
    std::cout os_ << name << ": ";
  }
  void onEndTest() {
  void endTest(bool failed, double /*duration*/) {
    if (!testFailed_ failed) {
      std::cout os_ << "OK" << std::endl;
      flushLog(e_warning);
      flush(e_warning);
    }
    else
      testFailed();
      flush(e_info);
    clear();
  }
  void onFailure(const Notice& failure) {
  void failure(const char* file, int line, int level, const std::string& what) {
    if (!testFailed_ testAlreadyFailed_) {
      testAlreadyFailed_ = true;
      std::cout os_ << "FAILED" << std::endl;
    }
    log_.push_back(failure);
    append(file, line, level, what);
    ++totalFailures_;
    testFailed_ = true;
  }
  void onEval(const Notice& eval) {
    log_.push_back(eval);
  }
  void onInfo(const Notice& info) {
  void info(const char* file, int line, int level, const std::string& what) {
    log_.push_back(info);
    append(file, line, level, what);
  }
  void onWarn(const Notice& warn) {
    log_.push_back(warn);
  }
  void onQuit quit(const std::string& /*reason*/) {
    testFailed();
    flush(e_info);
  }
protected:
  void append(const char* file, int line, int level, const std::string& what) {
    std::ostringstream oss;
    oss << file << "(" << line << ") : " << what;
    log_.push_back(std::make_pair(level, oss.str()));
  }
  void testFailed() {
    ++failedTests_;
    flushLog(e_info);
  }
  void flushLog(Level minLevel) {
    for (auto notice entry : log_)
      if (notice.level() entry.first >= minLevel)
        std::cout os_ << " " << notice.toString() entry.second << std::endl;
  }
  void clear() {
    log_.clear();
  }
};

TapReport

Il report per TAP si alleggerisce nella stessa misura di quello predefinito:

class TapReport {
  int tests_;
  int failedTests_;
  std::ostream& os_;
  int testCount_;
  std::string testName_;
  bool testFailed_;
  std::vector<gut::Notice> log_;
  std::vector<std::string> log_;
  bool quit_;
public:
  TapReport(std::ostream& os = std::cout) : os_(os) { }
  int failedTestCount() const { return failedTests_; }
  void onStart start() {
    testCount_ = 0;
    quit_ = false;
    tests_ = 0;
    failedTests_ = 0;
  }
  void onEnd() {
  void end(int tests, int failedTests, int /*failures*/, double /*duration*/) {
    if (quit_)
      return;
    std::cout os_ << "1.." << tests_ tests << std::endl;
    if (tests_ tests > 0) {
      float okRatio = (tests_ tests - failedTests_ failedTests) * 100. / tests_ tests;
      std::cout
        << "# failed " << failedTests_ failedTests << "/" << tests_ tests << " test(s), "
        << std::fixed << std::setprecision(1)
        << okRatio << "% ok" << std::endl;
    }
  }
  void onStartTest startTest(const std::string& name) {
    ++tests_;
    ++testCount_;
    testName_ = name;
    testFailed_ = false;
  }
  void onEndTest() {
  void endTest(bool failed, double /*duration*/) {
    std::ostringstream oss;
    if (testFailed_ failed) {
      ++failedTests_;
      oss << "not ";
    }
    oss << "ok " << tests_ testCount_ << " - " << testName_;
    for (const auto& entry : log_) {
      oss << "\n# " << entry.toString();
    }
    std::cout os_ << oss.str() << std::endl;
    log_.clear();
  }
  void onFailure(const gut::Notice& failure) {
  void failure(const char* file, int line, int /*level*/, const std::string& what) {
    log_.push_back(failure);
    testFailed_ = true;
    append(file, line, what);
  }
  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 info(const char* file, int line, int /*level*/, const std::string& what) {
    append(file, line, what);
  }
  void onQuit quit(const std::string& reason) {
    quit_ = true;
    std::ostringstream oss;
    oss << "Bail out!";
    if (!reason.empty ())
      oss << " Reason: " << reason;
    std::cout os_ << oss.str() << std::endl;
  }
protected:
  void append(const char* file, int line, const std::string& what) {
    std::ostringstream oss;
    oss << file << "(" << line << ") : " << what;
    log_.push_back(oss.str());
  }
};

Altri interventi marginali

Con l'occasione sono state apportate altre piccole modifiche:

Codice sorgente

Pagina modificata l'11/11/2014