risorse | good unit tests

Good Unit Tests /2

La prima parte di Good Unit Tests è incentrata sulla valutazione e la corretta rappresentazione dei valori citati nella macro CHECK; questa seconda parte affronta il problema della cattura delle eccezioni.

Cattura delle eccezioni

La verifica del sollevamento di un'eccezione è demandato ad una macro apposita, che si cura anche di controllare il tipo di eccezione sollevata:

int main() {
  // ...
  THROWS(f(), std::runtime_error); // fails if `f()` does not throw a std::runtime_error exception

  return 0;
}

Rinunciare al controllo del tipo di eccezione lanciata è molto limitante; come spiegarlo?

Il mancato lancio di una qualunque eccezione dev'essere trattata alla stessa stregua di un fallimento di CHECK:

#define THROWS(expr_, exception_) \
  do { \
    try { \
      (void)(expr_); \
      std::cout << __FILE__ << "(" << __LINE__ << "): " << #expr_ << " did not throw" << std::endl; \
    } catch(const exception_&) { \
    } \
  } while (0)

Analogamente, anche il lancio di un'eccezione di tipo diverso di quello atteso è un errore: conviene catturarle in THROW oppure lasciarle salire fino al main – dove dovrà comunque essere predisposto un blocco try/catch esterno per catturare le eccezioni non previste?

Cattura in THROWS

Il catch delle eccezioni inattese in THROWS consente di associare alle eccezioni stesse l'espressione che le ha prodotte, facilitando il debug. Risulta invece difficoltoso fornire qualunque informazione circa il tipo di eccezione catturata:

#define THROWS(expr_, exception_) \
  do { \
    try { \
      (void)(expr_); \
      std::cout << __FILE__ << "(" << __LINE__ << "): " << #expr_ << " did not throw" << std::endl; \
    } catch(const exception_&) { \
    } catch(...) { \
      std::cout << __FILE__ << "(" << __LINE__ << "): " << #expr_ << " threw an unexpected exception" << std::endl; \
    } \
  } while (0)

Fornire indicazioni seppur minimali circa l'eccezione catturata richiede la specializzazione della sezione di catch con qualcosa di simile a:

    ...
    } catch(const exception_&) { \
    } catch(const std::exception& e_) { \
      std::cout << __FILE__ << "(" << __LINE__ << "): " << #expr_ << " threw the unexpected exception \"" << e_.what() << "\"" << std::endl; \
    } catch(...) { \
      std::cout << __FILE__ << "(" << __LINE__ << "): " << #expr_ << " threw an unexpected unknown exception" << std::endl; \
    } \
    ...

L'esplicita cattura dell'eccezione std::exception rende inutilizzabile THROWS sullo stesso tipo di eccezione, poiché la sua espansione origina due clausole catch identiche:

int main() {
  // ...
  THROWS(f(), std::exception); // compiler error/warning!
  return 0;
}

Questa soluzione presenta l'interessante caratteristica di consentire la prosecuzione del test anche nel caso del sollevamento di un'eccezione non prevista. Ciò ovviamente al netto delle eccezioni sollevate all'esterno della macro THROWS, che causano invece l'interruzione del test.

Cattura in main

Posto che la presenza di un blocco try/catch globale per evitare la terminazione del programma in caso di eccezione imprevista è indispensabile, è appropriato affidarsi ad esso per la cattura delle eccezioni inattese sollevate all'interno di THROWS? Un vantaggio è che è perfettamente lecito predisporre il catch dell'eccezione generica std::exception e ricavarne il what(); certo, il contesto nel quale l'eccezione ha avuto origine è irrimediabilmente perso:

int main() {
  try {
    // ...
  } catch(const std::exception& e) {
    std::cout << "unexpected exception \"" << e.what() << "\" caught" << std::endl;
  } catch(...) {
    std::cout << "unexpected unknown exception caught" << std::endl;
  }
  return 0;
}

Implementazione

Relativamente all'implementazione di THROWS, ho scelto di adottare la seconda soluzione – con il catch esplicito di std::exception –, per la ragione che, trattandosi di unit-test, conviene specificare esattamente il tipo di eccezione attesa. Essendo std::exception astratta (o meglio, tale dev'essere considerata), va da sé che una tale eccezione non dovrebbe mai essere sollevata; per contro, è nota l'espressione che ha lanciato l'eccezione di tipo non atteso:

#include <stdexcept>

void fnThatNotThrows() {
}

int fnThatThrowsARuntimeError() {
  throw std::runtime_error("a runtime error");
}

int fnThatThrowsAnInt() {
  throw 42;
}

int main() {
  // ...
  THROWS(fnThatNotThrows(), std::runtime_error);
  THROWS(fnThatThrowsARuntimeError(), std::logic_error);
  THROWS(fnThatThrowsAnInt(), std::runtime_error);

  return 0;
}

// output:
// .../check.cpp(189): fnThatNotThrows() did not throw
// .../check.cpp(190): fnThatThrowsARuntimeError() threw an unexpected exception "a runtime error"
// .../check.cpp(191): fnThatThrowsAnInt() threw an unknown exception

Le eccezioni sollevate nel codice al di fuori di THROW – comprese le espressioni all'interno di CHECK – verranno invece catturate dal blocco try/catch più esterno, causando l'interruzione del test:

// ...
int main() {
  // ...
  CHECK(fnThatThrowsARuntimeError() == 1);
  CHECK(2 == 1); // won't execute

  return 0;
}

// output:
// unexpected exception "a runtime error" caught

Codice sorgente

Pagina modificata il 02/08/2012