risorse | good unit tests

Good Unit Tests /11

Questa parte (l'undicesima; 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 e qui la decima) è dedicata allo sviluppo della parte di controllo delle eccezioni.

Eccezioni

La parte di gut dedicata al controllo delle eccezioni è piuttosto rigida, essendo limitata alla sola direttiva THROWS; si nota in particolare l'assenza di metodi per:

A tal fine sono state definite tre nuove macro: THROWS_ANYTHING, THROWS_WITH_MESSAGE e THROWS_NOTHING.

THROWS_ANYTHING

La macro THROWS_ANYTHING consente di verificare che un'espressione solleva un qualche tipo di eccezione, indipendentemente dalla sua natura:

// file example.cpp
...

TEST("Out of range indexing throws exception") {
  RecentlyUsedList aListWithOneElement;
  aListWithOneElement.insert("one");

  THROWS(aListWithOneElement[1], std::out_of_range);
  THROWS_ANYTHING(aListWithOneElement[1]);
}

L'implementazione è banale:

// file gut.h
...

#define THROWS_ANYTHING(expr_) \
  do { \
    try { \
      (void)(expr_); \
      gut::theReport::failure(
        gut::NoThrowFailure(#expr_, __FILE__, __LINE__)); \
    } catch(...) { \
    } \
  } while (0)
// file test-gut.cpp
...

int main() {
  ...

  // test exceptions /2
  THROWS_ANYTHING(fnThatNotThrows());
  assert(lastFailure == "[error] fnThatNotThrows() did not throw");

  return 0;
}

THROWS_WITH_MESSAGE

A volte il verificare che un'espressione sollevi un particolare tipo di eccezione può non fornire il desiderato livello di dettaglio; la macro THROWS_WITH_MESSAGE consente di specificare, oltre al tipo dell'eccezione attesa, anche la stringa associata, intendendo con ciò il valore di ritorno del metodo what:

// file example.cpp
...

TEST("Out of range indexing throws exception") {
  RecentlyUsedList aListWithOneElement;
  aListWithOneElement.insert("one");

  THROWS(aListWithOneElement[1], std::out_of_range);
  THROWS_WITH_MESSAGE(aListWithOneElement[1], std::out_of_range, "invalid subscript");
}

Serve una tipologia di notifica dedicata, WrongExceptionMessageFailure, il cui scopo è di costruire un messaggio d'errore adeguato per questo nuovo caso. La macro THROWS_WITH_MESSAGE inoltra al report corrente una notifica di queste nel caso in cui il tipo di eccezione catturato è del tipo atteso, ma con un what non conforme:

// file gut.h
...

struct WrongTypedExceptionFailure : public Error {
  WrongTypedExceptionFailure(
    const char* expression,
    const std::exception& exception,
    const char* file,
    int line) : Error(file, line) {
      content()
        << expression
        << " threw an unexpected exception with message \""
        << exception.what()
        << "\"";
  }
};

struct WrongExceptionMessageFailure : public Error {
  WrongExceptionMessageFailure(
    const char* expression,
    const char* message,
    const char* expected,
    const char* file,
    int line) : Error(file, line) {
      content()
        << expression
        << " threw an exception with wrong message (expected \""
        << expected
        << "\", got \""
        << message
        << "\")";
  }
};
...

#define THROWS_WITH_MESSAGE(expr_, exception_, what_) \
  do { \
    try { \
      (void)(expr_); \
      gut::theReport::failure(gut::NoThrowFailure(#expr_, __FILE__, __LINE__)); \
    } catch(const exception_& e_) { \
      if (strcmp(e_.what(), static_cast<const char*>(what_)) != 0) \
        gut::theReport::failure(
          gut::WrongExceptionMessageFailure(#expr_, e_.what(), what_, __FILE__, __LINE__)); \
    } catch(const std::exception& e_) { \
      gut::theReport::failure(
        gut::WrongTypedExceptionFailure(#expr_, e_, __FILE__, __LINE__)); \
    } catch(...) { \
      gut::theReport::failure(
        gut::WrongExceptionFailure(#expr_, __FILE__, __LINE__)); \
    } \
  } while (0)
// file test-gut.cpp
...

    THROWS(fnThatThrowsARuntimeError(), std::logic_error);
    assert(lastFailure == "[error] fnThatThrowsARuntimeError()"
                          " threw an unexpected exception with message"
                          " \"a runtime error\"");
    ...

  THROWS_WITH_MESSAGE(fnThatThrowsARuntimeError(), std::runtime_error, "error");
  assert(lastFailure == "[error] fnThatThrowsARuntimeError()"
                        " threw an exception with wrong message"
                        " (expected \"error\", got \"a runtime error\")");

Relativamente alla classe WrongTypedExceptionFailure: con poca fatica si potrebbe riportare, nel messaggio d'errore, anche il tipo dell'eccezione attesa; non però quello dell'eccezione catturata, che sarebbe ben più interessante. Essendo il tipo dell'eccezione attesa comunque visibile nella clausola THROWS, tale implementazione al momento non trova giustificazione.

THROWS_NOTHING

Se viene sollevata un'eccezione inattesa, il test termina, e non è immediato determinare il punto d'origine. Vorrei allora assicurarmi che certe espressioni non sollevano eccezioni:

// file example.cpp
...

TEST("Insertion to empty list is retained") {
  RecentlyUsedList aListWithOneElement;
  THROWS_NOTHING(aListWithOneElement.insert("one"));

  CHECK(!aListWithOneElement.empty());
  CHECK(aListWithOneElement.size() == 1);
  CHECK(aListWithOneElement[0] == "one");
}

Giunti a questo punto, è necessario fare un pò di ordine, per evitare di commettere l'errore di utilizzare le notifiche UnexpectedExceptionFailure e UnknownExceptionFailure per i casi di «eccezione inattesa» — rispettivamente catch (const std::exception&) e catch (...) — che la nuova macro dovrà gestire.

Le due ExceptionFailure citate sono attualmente utilizzate dal sistema per catturare le eccezioni inattese emesse dal codice di test; per quanto era stato a suo tempo stabilito, sono di tipo Fatal, così da causare la sospensione del test in caso di attivazione.

Per analogia con quanto avviene per le macro THROWS già disponibili, THROWS_NOTHING deve invece segnalare delle notifiche di tipo Error, in modo da comunicare sì l'incongruenza, ma consentire la prosecuzione del test. Per questa ragione, le due notifiche esistenti mutano la loro natura Fatal in Error, per essere utilizzate dalla nuova macro; nel contempo si definiscono due nuove notifiche, FatalUnexpectedExceptionFailure e FatalUnknownExceptionFailure per assicurare la sospensione del test in caso di eccezioni inattese:

// file gut.h
...

struct UnexpectedExceptionFailure : public FatalError {
  UnexpectedExceptionFailure(
    const std::exception& exception,
    const char* file,
    int line) : FatalError(file, line) {
      content()
        << "unexpected exception with message \""
        << exception.what()
        << "\" caught";
  }
};

struct UnknownExceptionFailure : public FatalError {
  UnknownExceptionFailure(const char* file, int line) : FatalError(file, line) {
    content() << "unknown exception caught";
  }
};

struct FatalUnexpectedExceptionFailure : public Fatal {
  FatalUnexpectedExceptionFailure(
    const std::exception& exception,
    const char* file,
    int line) : Fatal(file, line) {
      content()
        << "unexpected exception with message \""
        << exception.what()
        << "\" caught";
  }
};

struct FatalUnknownExceptionFailure : public Fatal {
  FatalUnknownExceptionFailure(const char* file, int line) : Fatal(file, line) {
    content() << "unknown exception caught";
  }
};
...

#define THROWS_NOTHING(expr_) \
  do { \
    try { \
      (void)(expr_); \
    } catch(const std::exception& e_) { \
      gut::theReport::failure(
        gut::UnexpectedExceptionFailure(e_, __FILE__, __LINE__)); \
    } catch(...) { \
      gut::theReport::failure(
        gut::UnknownExceptionFailure(__FILE__, __LINE__)); \
    } \
  } while (0)
...

int runTests_() {
  ...
    } catch(const std::exception& e_) {
      gut::theReport::failure(
        gut::FatalUnexpectedExceptionFailure(e_, __FILE__, __LINE__));
    } catch(...) {
      gut::theReport::failure(
        gut::FatalUnknownExceptionFailure(__FILE__, __LINE__));
    }
  ...
}

// file test-gut.cpp
...

int main() {
  ...

  assert(lastFailure == "[fatalerror] unexpected exception with message \"a runtime error\" caught");
  ...


  THROWS_NOTHING(fnThatThrowsARuntimeError());
  assert(lastFailure == "[error] unexpected exception with message \"a runtime error\" caught");

  THROWS_NOTHING(fnThatThrowsAnInt());
  assert(lastFailure == "[error] unknown exception caught");

  return 0;
}

REQUIRE_THROWS

Le clausole THROWS emettono delle notifiche d'errore, alla stessa stregua di CHECK. Potrebbe risultare utile introdurre una nuova classe di clausole THROWS che emettano delle notifiche di fallimento critico, esattamente come fa REQUIRE. Si potrebbero distinguere dalle esistenti per il prefisso REQUIRE_:

// file gut.h
...

struct FatalNoThrowFailure : public Fatal {
  FatalNoThrowFailure(const char* expression, const char* file, int line) : Fatal(file, line) {
    content() << expression << " did not throw";
  }
};
...

#define REQUIRE_THROWS_ANYTHING(expr_) \
  do { \
    bool threw_ = false; \
    try { \
      (void)(expr_); \
    } catch(...) { \
      threw_ = true; \
    } \
    if (!threw_) { \
      gut::theReport::failure(
        gut::FatalNoThrowFailure(#expr_, __FILE__, __LINE__)); \
      throw gut::AbortTest(); \
    } \
  } while (0)

L'implementazione della clausola REQUIRE_THROWS suggerisce una fattorizzazione del codice:

struct FatalWrongTypedExceptionFailure : public Fatal {
  FatalWrongTypedExceptionFailure(
    const char* expression,
    const std::exception& exception,
    const char* file,
    int line) : Fatal(file, line) {
      content()
        << expression
        << " threw an unexpected exception with message \""
        << exception.what()
        << "\"";
  }
};

struct FatalWrongExceptionFailure : public Fatal {
  FatalWrongExceptionFailure(
    const char* expression,
    const char* file,
    int line) : Fatal(file, line) {
      content() << expression << " threw an unknown exception";
  }
};

#define REQUIRE_THROWS_(expr_, exception_, prefix_, abort_) \
  do { \
    bool catched_ = false; \
    try { \
      (void)(expr_); \
      gut::theReport::failure(
        gut::prefix_ ## NoThrowFailure(#expr_, __FILE__, __LINE__)); \
    } catch(const exception_&) { \
      catched_ = true; \
    } catch(const std::exception& e_) { \
      gut::theReport::failure(
        gut::prefix_ ## WrongTypedExceptionFailure(#expr_, e_, __FILE__, __LINE__)); \
    } catch(...) { \
      gut::theReport::failure(
        gut::prefix_ ## WrongExceptionFailure(#expr_, __FILE__, __LINE__)); \
    } \
    if (!catched_ && abort_) \
      throw gut::AbortTest(); \
  } while (0)

#define THROWS(expr_, exception_) \
  THROWS_(expr_, exception_, , false)

#define REQUIRE_THROWS(expr_, exception_) \
  THROWS_(expr_, exception_, Fatal, true)

Per REQUIRE_THROWS_WITH_MESSAGE si procede in maniera analoga:

struct FatalWrongExceptionMessageFailure : public Fatal {
  FatalWrongExceptionMessageFailure(
    const char* expression,
    const char* message,
    const char* expected,
    const char* file,
    int line) : Fatal(file, line) {
      content()
        << expression
        << " threw an exception with wrong message (expected \""
        << expected
        << "\", got \""
        << message
        << "\")";
  }
};

#define THROWS_WITH_MESSAGE_(expr_, exception_, what_, prefix_, abort_) \
  do { \
    bool catched_ = false; \
    try { \
      (void)(expr_); \
      gut::theReport::failure(
        gut::prefix_ ## NoThrowFailure(#expr_, __FILE__, __LINE__)); \
    } catch(const exception_& e_) { \
      if (strcmp(e_.what(), static_cast<const char*>(what_)) != 0) \
        gut::theReport::failure(
          gut::prefix_ ## WrongExceptionMessageFailure(#expr_, e_.what(), what_, __FILE__, __LINE__)); \
      else \
      catched_ = true; \
    } catch(const std::exception& e_) { \
      gut::theReport::failure(
        gut::prefix_ ## WrongTypedExceptionFailure(#expr_, e_, __FILE__, __LINE__)); \
    } catch(...) { \
      gut::theReport::failure(
        gut::prefix_ ## WrongExceptionFailure(#expr_, __FILE__, __LINE__)); \
    } \
    if (!catched_ && abort_) \
      throw gut::AbortTest(); \
  } while (0)

#define THROWS_WITH_MESSAGE(expr_, exception_, what_) \
  THROWS_WITH_MESSAGE_(expr_, exception_, what_, , false)

#define REQUIRE_THROWS_WITH_MESSAGE(expr_, exception_, what_) \
  THROWS_WITH_MESSAGE_(expr_, exception_, what_, Fatal, true)

Infine, REQUIRE_THROWS_NOTHING:

#define REQUIRE_THROWS_NOTHING(expr_) \
  do { \
    bool threw_ = true; \
    try { \
      (void)(expr_); \
      threw_ = false; \
    } catch(const std::exception& e_) { \
      gut::theReport::failure(gut::FatalUnexpectedExceptionFailure(e_, __FILE__, __LINE__)); \
    } catch(...) { \
      gut::theReport::failure(gut::FatalUnknownExceptionFailure(__FILE__, __LINE__)); \
    } \
    if (threw_) \
      throw gut::AbortTest(); \
  } while (0)

TODO: Riflettere sulla possibilità di unificare prefix_ e abort_; la cosa probabimente impatta in modo significativo sulla gerarchia Notice.

Codice sorgente

Pagina modificata il 28/08/2014