3. C++ et le monde extérieur




3.8. Tests, CPPUnit

Avant de pouvoir être mis en production les programmes doivent être testés afin d'établir leur fiabilité et leur tolérance aux fautes.

On recommande aux programmeurs de mettre en place des tests qui ont pour but de débusquer les bogues inhérants à chaque programme.

En Génie Logiciel notamment une bonne pratique de développement est le Développement Dirigé par les Tests (Test Driven Development) qui consiste à développer les programmes de tests avant d'écrire les classes de l'application.

Il existe différents types de tests, on peut citer par exemple :

Pour réaliser les tests unitaires on peut utiliser le framework xUnit notamment le CppUnit. Pour cela il faut installer les librairies libcppunit-1.13-0 et libcppunit-dev.

Voir également le site web cppunit

3.8.1. Les tests unitaires avec CPPUnit

Voici un exemple de base qui introduit des méthodes de test pour la classe vector de la STL :

Par défaut, les méthodes setUp et tearDown permettent d'allouer + initialiser et de libérer les ressources. Dans le cas présent je crée un vector<int> que je remplis avec 100 entiers de 1 à 100.

A l'intérieur de chaque méthode de test on utilise la macro instruction CPPUNIT_ASSERT qui permet de vérifier si une condition est validée ou non.

  1. #ifndef TEST_VECTOR_H
  2. #define TEST_VECTOR_H
  3.  
  4. #include "cppunit/TestCase.h"
  5. #include "cppunit/TestFixture.h"
  6. #include "cppunit/TestCaller.h"
  7. #include "cppunit/TestResult.h"
  8. #include "cppunit/TestSuite.h"
  9. #include "cppunit/CompilerOutputter.h"
  10. #include "cppunit/XmlOutputter.h"
  11. #include "cppunit/ui/text/TestRunner.h"
  12.  
  13. #include <vector>
  14. using namespace std;
  15.  
  16. // Use those macros and repeat the class name three times in
  17. // CLASS_NAME, CLASS_NAME_STRING
  18. #define CLASS_NAME vector
  19. #define CLASS_NAME_STRING "vector"
  20. #define OUTPUT_XML_FILE "output.xml"
  21.  
  22.  
  23. #define TEST_DECL(x) void test_##x()
  24. #define TEST_ADD(name) \
  25.     suite->addTest(new CppUnit::TestCaller<TestFixture>("test_"#name, \
  26.         &TestFixture::test_##name));
  27.  
  28.  
  29. class TestFixture : public CppUnit::TestFixture {
  30. private:
  31.     vector<int> v;
  32.    
  33. public:
  34.     void setUp();
  35.     void tearDown();
  36.  
  37.  
  38.     TEST_DECL(push_back);
  39.     TEST_DECL(erase);
  40.     TEST_DECL(erase_insert);
  41.     TEST_DECL(fail1);
  42.     TEST_DECL(fail2);
  43. };
  44.  
  45. #endif /* TEST_VECTOR_H */
  46.  
  1. /*
  2.  * test_vector.cpp
  3.  */
  4.  
  5. #include "test_vector_1.h"
  6. #include <algorithm>
  7. #include <iterator>
  8. #include <numeric>
  9. using namespace std;
  10.  
  11. // ----------------------------------------------
  12.  
  13. const int MAX_VALUES = 100;
  14.  
  15. /**
  16.  * setUp: function called before each test
  17.  */
  18. void TestFixture::setUp() {
  19.     for (int i=1; i<=MAX_VALUES; ++i) {
  20.         v.push_back(i);
  21.     }
  22. }
  23.  
  24. /**
  25.  * setUp: function called after each test
  26.  */
  27. void TestFixture::tearDown() {
  28.     v.clear();
  29. }
  30.  
  31. /**
  32.  * test that all values are present by computing
  33.  * sum i=1,size of v[i] which is supposed to be
  34.  * equal to size*(size+1)/2
  35.  */
  36. void TestFixture::test_push_back() {
  37.     // vector has been filled by method 'setUp'
  38.     int total = accumulate(v.begin(), v.end(), 0);
  39.    
  40.     int expected_total = (MAX_VALUES * (MAX_VALUES+1)) / 2;
  41.    
  42.     CPPUNIT_ASSERT(total == expected_total);   
  43. }
  44.  
  45. /**
  46.  * test that the value erased are not present
  47.  */
  48. void TestFixture::test_erase() {
  49.     // vector has been filled by method 'setUp'
  50.    
  51.     // remove first and last values
  52.     v.erase(v.begin());
  53.     v.erase(v.end()-1);
  54.    
  55.     int total = accumulate(v.begin(), v.end(), 0);
  56.    
  57.     int expected_total = (MAX_VALUES * (MAX_VALUES+1)) / 2;
  58.     expected_total -= (1 + MAX_VALUES);
  59.    
  60.     CPPUNIT_ASSERT(total == expected_total);
  61. }
  62.  
  63. /**
  64.  * test erase followed by insert of the value
  65.  *
  66.  * randomly select values, remove them and reinsert them
  67.  *
  68.  */
  69. void TestFixture::test_erase_insert() {
  70.     // vector has been filled by method 'setUp'
  71.     for (int i=0; i<v.size(); ++i) {
  72.         int index = rand() % v.size();
  73.         int value = v[index];
  74.         v.erase(v.begin() + index);
  75.         v.insert(v.begin() + rand() % v.size(), value);
  76.    
  77.     }
  78.     int total = accumulate(v.begin(), v.end(), 0);
  79.    
  80.     int expected_total = (MAX_VALUES * (MAX_VALUES+1)) / 2;
  81.    
  82.     CPPUNIT_ASSERT(total == expected_total);
  83. }
  84.  
  85. /**
  86.  * Test that will fail, used for example purpose
  87.  */
  88. void TestFixture::test_fail1() {
  89.     CPPUNIT_ASSERT(0 == 1);
  90. }
  91.  
  92. /**
  93.  * Test that will fail, used for example purpose
  94.  */
  95. void TestFixture::test_fail2() {
  96.     CPPUNIT_ASSERT(true == false);
  97. }
  98.  
  99.  
  100. /**
  101.  * declare suite of tests
  102.  *
  103.  */
  104. CppUnit::TestSuite *make_suite() {
  105.     CppUnit::TestSuite *suite = new CppUnit::TestSuite(CLASS_NAME_STRING);
  106.     cout << "==============================================" << endl;
  107.     cout << "TEST " << suite->getName() << " (" << __FILE__ << ")" << endl;
  108.     cout << "==============================================" << endl;
  109.  
  110.     TEST_ADD(push_back);
  111.     TEST_ADD(erase);
  112.     TEST_ADD(erase_insert);
  113.     TEST_ADD(fail1);
  114.     TEST_ADD(fail2);
  115.  
  116.     return suite;
  117. }
  118.  
  119. /**
  120.  * main function
  121.  */
  122. int main(int argc, char *argv[]) {
  123.     CppUnit::TextUi::TestRunner runner;
  124.     CppUnit::XmlOutputter *xml_outputter = NULL;
  125.  
  126.     // create suite
  127.     CppUnit::TestSuite *suite = make_suite();
  128.     runner.addTest(suite);
  129.  
  130.     // set output format as text
  131.     runner.setOutputter(new CppUnit::CompilerOutputter(&runner.result(), cout));
  132.    
  133.     // run all tests
  134.     runner.run();
  135.    
  136.     // output as XML also
  137.     ofstream xml_out(OUTPUT_XML_FILE);
  138.     xml_outputter = new CppUnit::XmlOutputter(&runner.result(), xml_out);
  139.     xml_outputter->write();
  140.     xml_out.close();
  141.    
  142.     return 0;
  143. }
  144.  
  145.  

On compile avec les options suivantes :

g++ -o test_vector_1.exe test_vector_1.cpp -lcppunit

Puis on exécute en lançant le programme, voici le résultat de la sortie en mode texte :

./test_vector_1.exe
==============================================
TEST vector (test_vector_1.cpp)
==============================================
....F.F

test_vector_1.cpp:89:Assertion
Test name: test_fail1
assertion failed
- Expression: 0 == 1

test_vector_1.cpp:96:Assertion
Test name: test_fail2
assertion failed
- Expression: true == false

Failures !!!
Run: 5   Failure total: 2   Failures: 2   Errors: 0

Le programme reporte les assertions non vérifiées CPPUNIT_ASSERT(0 == 1); de la ligne 89 du programme et CPPUNIT_ASSERT(true == false); de la ligne 96.

Voici la sortie au format XML :

  1. <?xml version="1.0" encoding='ISO-8859-1' standalone='yes' ?>
  2. <TestRun>
  3.   <FailedTests>
  4.     <FailedTest id="4">
  5.       <Name>test_fail1</Name>
  6.       <FailureType>Assertion</FailureType>
  7.       <Location>
  8.         <File>test_vector_0.cpp</File>
  9.         <Line>89</Line>
  10.       </Location>
  11.       <Message>assertion failed
  12. - Expression: 0 == 1
  13. </Message>
  14.     </FailedTest>
  15.     <FailedTest id="5">
  16.       <Name>test_fail2</Name>
  17.       <FailureType>Assertion</FailureType>
  18.       <Location>
  19.         <File>test_vector_0.cpp</File>
  20.         <Line>96</Line>
  21.       </Location>
  22.       <Message>assertion failed
  23. - Expression: true == false
  24. </Message>
  25.     </FailedTest>
  26.   </FailedTests>
  27.   <SuccessfulTests>
  28.     <Test id="1">
  29.       <Name>test_push_back</Name>
  30.     </Test>
  31.     <Test id="2">
  32.       <Name>test_erase</Name>
  33.     </Test>
  34.     <Test id="3">
  35.       <Name>test_erase_insert</Name>
  36.     </Test>
  37.   </SuccessfulTests>
  38.   <Statistics>
  39.     <Tests>5</Tests>
  40.     <FailuresTotal>2</FailuresTotal>
  41.     <Errors>0</Errors>
  42.     <Failures>2</Failures>
  43.   </Statistics>
  44. </TestRun>
  45.  

On peut ensuite extraire les méthodes qui ont échoué grâce à un utilitaire comme xpath du package libxml-xpath-perl :

sudo apt-get install libxml-xpath-perl
> xpath -q -e "//FailedTest/Name/text()" output.xml
test_fail1
test_fail2

Voici un script shell qui permet de récupérer les noms des méthodes qui ont échoué tout en vérifiant que tous les tests ont été exécutés :

  1. #!/bin/sh
  2. files=`ls test_*_1.exe`
  3.  
  4. for f in $files ; do
  5.     ./$f >tmp.txt
  6.     x=`xpath -q -e "//FailedTest/Name/text()" output.xml`
  7.     echo "===================";
  8.     echo "$f"
  9.     echo "$x" | awk '{ print "-", $1; }'
  10.     echo "===================";
  11. done
  12.  
> ./cppunit_failures_1.sh 
===================
test_vector_1.exe
- test_fail1
- test_fail2
===================

3.8.2. Version avancée

Voici une version avancée qui prend en compte le nombre de tests et qui permet de vérifier que l'ensemble des tests à été exécuté.

En effet, si l'une methode échoue et provoque par exemple un segmentation fault, les autres méthodes ne seront pas exécutées et il ne sera pas possible de le savoir. On risque donc de penser que tout va bien alors que certains tests n'ont pas été exécutés.

  1. #ifndef TEST_VECTOR_H
  2. #define TEST_VECTOR_H
  3.  
  4. #include "cppunit/TestCase.h"
  5. #include "cppunit/TestFixture.h"
  6. #include "cppunit/TestCaller.h"
  7. #include "cppunit/TestResult.h"
  8. #include "cppunit/TestSuite.h"
  9. #include "cppunit/CompilerOutputter.h"
  10. #include "cppunit/XmlOutputter.h"
  11. #include "cppunit/ui/text/TestRunner.h"
  12.  
  13. #include <vector>
  14. using namespace std;
  15.  
  16. // Use those macros and repeat the class name three times in
  17. // CLASS_NAME, CLASS_NAME_STRING, OUTPUT_XML_FILE
  18. #define CLASS_NAME vector
  19. #define CLASS_NAME_STRING "vector"
  20. #define OUTPUT_XML_FILE "test_vector.xml"
  21.  
  22.  
  23. #define TEST_DECL(x) void test_##x()
  24. #define ADD_TEST(name) \
  25.     suite->addTest(new CppUnit::TestCaller<TestFixture>("test_"#name, \
  26.         &TestFixture::test_##name));
  27.  
  28.  
  29. class TestFixture : public CppUnit::TestFixture {
  30. private:
  31.     vector<int> v;
  32.    
  33. public:
  34.     void setUp();
  35.     void tearDown();
  36.  
  37.  
  38.     TEST_DECL(push_back);
  39.     TEST_DECL(erase);
  40.     TEST_DECL(erase_insert);
  41.     TEST_DECL(fail1);
  42.     TEST_DECL(fail2);
  43.     TEST_DECL(div_by_zero);
  44. };
  45.  
  46. #endif /* TEST_VECTOR_H */
  47.  
  1. /*
  2.  * test_vector_2.cpp
  3.  */
  4.  
  5. #include "test_vector_2.h"
  6. #include <algorithm>
  7. #include <iterator>
  8. #include <numeric>
  9. using namespace std;
  10. #include <getopt.h>
  11. #include <cstring>
  12. #include <signal.h>
  13. #include <err.h>
  14. #include <cstdlib>
  15. #include <cstdio>
  16.  
  17. void signal_handler(int sig) {
  18.     cerr << endl << "caught signal " << sig << endl;
  19.     cerr.flush();  
  20.     return ;
  21. }
  22.  
  23. void set_signal_handler() {
  24.     signal( SIGABRT, signal_handler);
  25.     signal( SIGHUP, signal_handler);
  26.     signal( SIGILL, signal_handler);
  27.  }
  28.  
  29. // ----------------------------------------------
  30. // description of possible arguments
  31. // that can be used in
  32. // - short format : -m 1
  33. // - or long format : --method=1
  34. // ----------------------------------------------
  35. static struct option long_options[] = {
  36.   {"nbr-tests",         no_argument, 0, 'n'},
  37.   {"output-format",     required_argument, 0, 'f'},
  38.   {0,0,0,0}
  39. };
  40.  
  41. typedef struct {
  42.     string output_file;
  43.     int  output_format;
  44.     bool flag_get_nbr_tests;
  45. } Parameters;
  46.  
  47. Parameters params;
  48.  
  49. const char *params_format_allowed[] = { "text", "xml", NULL };
  50.  
  51. int get_allowed_value(const char *s, const char *tab[]) {
  52.     for (int i=0; tab[i] != NULL; ++i) {
  53.         if (strcmp(s,tab[i])==0) return i;
  54.     }
  55.     return -1;
  56. }
  57.  
  58. void usage(char *prog_name) {
  59.     cout << prog_name << endl;
  60.     cout << "--output-format=text|xml" << endl;
  61.     cout << "--nbr-tests" << endl;
  62.    
  63.     exit(EXIT_FAILURE);
  64. }
  65.  
  66. // ----------------------------------------------
  67.  
  68. const int MAX_VALUES = 100;
  69.  
  70. /**
  71.  * setUp: function called before each test
  72.  */
  73. void TestFixture::setUp() {
  74.     for (int i=1; i<=MAX_VALUES; ++i) {
  75.         v.push_back(i);
  76.     }
  77. }
  78.  
  79. /**
  80.  * setUp: function called after each test
  81.  */
  82. void TestFixture::tearDown() {
  83.     v.clear();
  84. }
  85.  
  86. /**
  87.  * test that all values are present by computing
  88.  * sum i=1,size of v[i] which is supposed to be
  89.  * equal to size*(size+1)/2
  90.  */
  91. void TestFixture::test_push_back() {
  92.     // vector has been filled by method 'setUp'
  93.     int total = accumulate(v.begin(), v.end(), 0);
  94.    
  95.     int expected_total = (MAX_VALUES * (MAX_VALUES+1)) / 2;
  96.    
  97.     CPPUNIT_ASSERT(total == expected_total);   
  98. }
  99.  
  100. /**
  101.  * test that the value erased are not present
  102.  */
  103. void TestFixture::test_erase() {
  104.     // vector has been filled by method 'setUp'
  105.    
  106.     // remove first and last values
  107.     v.erase(v.begin());
  108.     v.erase(v.end()-1);
  109.    
  110.     int total = accumulate(v.begin(), v.end(), 0);
  111.    
  112.     int expected_total = (MAX_VALUES * (MAX_VALUES+1)) / 2;
  113.     expected_total -= (1 + MAX_VALUES);
  114.    
  115.     CPPUNIT_ASSERT(total == expected_total);
  116. }
  117.  
  118. /**
  119.  * test erase followed by insert of the value
  120.  *
  121.  * randomly select values, remove them and reinsert them
  122.  *
  123.  */
  124. void TestFixture::test_erase_insert() {
  125.     // vector has been filled by method 'setUp'
  126.     for (int i=0; i<v.size(); ++i) {
  127.         int index = rand() % v.size();
  128.         int value = v[index];
  129.         v.erase(v.begin() + index);
  130.         v.insert(v.begin() + rand() % v.size(), value);
  131.    
  132.     }
  133.     int total = accumulate(v.begin(), v.end(), 0);
  134.    
  135.     int expected_total = (MAX_VALUES * (MAX_VALUES+1)) / 2;
  136.    
  137.     CPPUNIT_ASSERT(total == expected_total);
  138. }
  139.  
  140. /**
  141.  * Test that will fail, used for example purpose
  142.  */
  143. void TestFixture::test_fail1() {
  144.     CPPUNIT_ASSERT(0 == 1);
  145. }
  146.  
  147. /**
  148.  * Test that will fail, used for example purpose
  149.  */
  150. void TestFixture::test_fail2() {
  151.     CPPUNIT_ASSERT(true == false);
  152. }
  153.  
  154. /**
  155.  * Test that will fail with a division by zero,
  156.  * used for example purpose
  157.  */
  158. void TestFixture::test_div_by_zero() {
  159.     int a = 0;
  160.     int b = 10;
  161.     int c = b / a;
  162.     CPPUNIT_ASSERT(c != 0);
  163. }
  164.  
  165.  
  166. /**
  167.  * declare suite of tests
  168.  *
  169.  */
  170. CppUnit::TestSuite *make_suite() {
  171.     CppUnit::TestSuite *suite = new CppUnit::TestSuite(CLASS_NAME_STRING);
  172.     cout << "==============================================" << endl;
  173.     cout << "TEST " << suite->getName() << " (" << __FILE__ << ")" << endl;
  174.     cout << "==============================================" << endl;
  175.  
  176.     ADD_TEST(push_back);
  177.     ADD_TEST(erase);
  178.     ADD_TEST(erase_insert);
  179.     ADD_TEST(fail1);
  180.     ADD_TEST(fail2);
  181.     ADD_TEST(div_by_zero);
  182.  
  183.     return suite;
  184. }
  185.  
  186. /**
  187.  * main function
  188.  */
  189. int main(int argc, char *argv[]) {
  190.     CppUnit::TextUi::TestRunner runner;
  191.     CppUnit::XmlOutputter *xml_outputter = NULL;
  192.  
  193.     params.output_file = "";
  194.     params.output_format = 0; // text
  195.     params.flag_get_nbr_tests = false;
  196.    
  197.     int option_index;
  198.     while (true) {
  199.         option_index = 0;
  200.         int c = getopt_long(argc, argv, "f:n", long_options, &option_index);
  201.         if (c == -1) break;
  202.  
  203.         switch(c) {
  204.           case 'n':
  205.             params.flag_get_nbr_tests = true;
  206.             break;
  207.           case 'f':
  208.             params.output_format = get_allowed_value(optarg, params_format_allowed);
  209.             break;
  210.         }
  211.     }
  212.    
  213.     if (params.output_format == -1) {
  214.         usage(argv[0]);
  215.     }
  216.    
  217.    
  218.     set_signal_handler();
  219.  
  220.     // create suite
  221.     CppUnit::TestSuite *suite = make_suite();
  222.     runner.addTest(suite);
  223.    
  224.     if (params.output_format == 1) {
  225.         cout << "xml file=" << OUTPUT_XML_FILE << endl;
  226.     }
  227.    
  228.     if (params.flag_get_nbr_tests == true) {
  229.         const std::vector<CppUnit::Test *>& tests = suite->getTests();
  230.         cout << "nbr_tests=" << tests.size() << endl;
  231.         exit(EXIT_SUCCESS);
  232.     }
  233.      
  234.     // set output format as text
  235.     runner.setOutputter(new CppUnit::CompilerOutputter(&runner.result(), cout));
  236.    
  237.     // run all tests
  238.     runner.run();
  239.    
  240.     if (params.output_format == 1) {
  241.         ofstream xml_out(OUTPUT_XML_FILE);
  242.         xml_outputter = new CppUnit::XmlOutputter(&runner.result(), xml_out);
  243.         xml_outputter->write();
  244.         xml_out.close();
  245.     }
  246.    
  247.     return 0;
  248. }
  249.  
  250.  
  1. #!/bin/sh
  2.  
  3. execute_test()
  4. {
  5.     ./$f --output-format=xml >tmp2.txt
  6. }
  7.  
  8. files=`ls test*_2.exe`
  9. for f in $files ; do
  10.     ./$f --nbr-test --output-format=xml >tmp.txt
  11.     nbr=`cat tmp.txt | grep "^nbr_tests=" | cut -d'=' -f2`
  12.    
  13.     # SIGHUP SIGQUIT SIGINT SIGABRT
  14.     echo "execute $f " 
  15.     trap execute_test 1 2 3 6
  16.    
  17.     xml=`cat tmp.txt | grep "^xml file=" | cut -d'=' -f2`
  18.     r=""
  19.     if test -f $xml ; then
  20.         x=`xpath -q -e "//FailedTest/Name/text()" $xml`
  21.         r=`xpath -q -e "//Statistics//Tests/text()" $xml`
  22.         echo "===================";
  23.         if test -n "$x" ; then
  24.             echo "$f"
  25.             echo "$x" | awk '{ print "-", $1; }'
  26.         fi
  27.         if test $nbr != $r ; then
  28.             echo "failed ! $nbr tests expected, only $r performed"
  29.         fi
  30.         echo "===================";
  31.     else
  32.         echo "===================";
  33.         echo "$f FAILURE ! Program did not terminate"
  34.         echo "===================";
  35.     fi
  36. done
  37.  
> ./cppunit_failures_1.sh 
===================
test_vector_1.exe
- test_fail1
- test_fail2
===================

3.9. Tests, Google Test

Récupérer googletest-release-1.7.0.tar.gz sur googletest, puis dans le répertoire où le fichier a été télécharger taper :

sudo tar -xvzf googletest-release-1.7.0.tar.gz -C /opt
sudo su
cd /opt/googletest-release-1.7.0
cmake -DBUILD_SHARED_LIBS=ON -Dgtest_build_samples=ON -G "Unix Makefiles" 
make
cp -R include/gtest /usr/include
cp lib*.a /usr/lib 
ldconfig
exit

Créer le fichier suivant :

  1. #include <gtest/gtest.h>
  2.  
  3. TEST(MathTest, TwoPlusTwoEqualsFour) {
  4.     EXPECT_EQ(2 + 2, 4);
  5. }
  6.  
  7. TEST(MathTest, ThatWillFail) {
  8.     EXPECT_EQ(0, 1);
  9. }
  10.  
  11.  
  12. int main(int argc, char **argv) {
  13.     ::testing::InitGoogleTest( &argc, argv );
  14.     return RUN_ALL_TESTS();
  15. }
  16.  
  17.  

et compiler avec :

g++ -o my_gtest.exe my_gtest.cpp -lgtest -lgtest_main -lpthread