La couverture de code ou Code coverage en anglais, est une technique visant à mesurer le taux de code source qui est couvert par les différents tests d'un logiciel.
Si par exemple, j'ai un fichier source contenant une fonction qui a 10 lignes de code et que mes tests unitaires passeront sur 8 d'entre elles alors j'aurai une couverture de code de 80%.
La mise en place d'un système de couverture de code permet au développeur de prendre connaissance des parties de codes qui sont couvertes par les tests et de celles qui ne le sont pas.
Les outils qui seront utilisés pour la démo seront Gcov, Lcov et CMake.
GCov est un utilitaire qui fait parti de la suite GNU Compiler Collection (GCC) et qui permettant de générer le nombre exact de fois que les instructions d'un programme ont été exécutées.
LCov est un outil qui permet de rendre graphiquement les résultats acquis via l'application GCov.
CMake quant à lui n'a plus besoin de présentation surtout dans le monde de l'open source, alors ce sera l'outil de build system que nous nous servirons.
Le projet ci-dessous nous servira d'exemple. C'est un projet tout simple qui a pour but d'afficher le nom et prénom d'un employé à l'écran.
Nous avons notre application principale dans le dossier app, une bibliothèque partagée de nos modèles de données dans le dossier models ainsi que nos tests unitaires, sous format Google Test, dans le dossier test.
L'application principale (app) créer tout simplement un employé puis l'affiche à l'écran.
app/src/main.cpp
#include "employee.h" #include <iostream> using namespace std; int main(int argc, char *argv[]) { Employee myEmployee(1, "Joe", "Blow"); cout << myEmployee << "\n"; return 0; }
Allons voir le contenu de la bibliothèque partagée de plus près et plus précisément la classe Employee :
- Trois champs privés: id, firstname et lastName.
- 1 constructeur pour initialiser nos trois champs
- Trois méthodes getter afin de pouvoir récupérer les valeurs de nos champs
- Surcharge de l'opérateur << dans le but d'afficher facilement un employé (prénom et nom).
models/include/employee.h
#pragma once #include <iostream> #include <string> class Employee { public: Employee(unsigned int id, const std::string & firstName, const std::string &lastName); unsigned int getId() const; const std::string &getFirstName() const; const std::string &getLastName() const; friend std::ostream& operator<<(std::ostream &os, const Employee &emp); private: unsigned int id; std::string firstName; std::string lastName; };
models/src/employee.cpp
#include "employee.h" #include <stdexcept> using namespace std; Employee::Employee(unsigned int id, const std::string & firstName, const std::string &lastName) : id {id}, firstName {firstName}, lastName {lastName} { if (id == 0) { throw invalid_argument("id must be greater than zero."); } } unsigned int Employee::getId() const { return id; } const std::string& Employee::getFirstName() const { return firstName; } const std::string& Employee::getLastName() const { return lastName; } std::ostream& operator<<(std::ostream &os, const Employee &emp) { os << emp.firstName << " " << emp.lastName; return os; }
Si l'on compile et que l'on roule l'application, on obtient le résultat suivant :
jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo$ mkdir build jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo$ cd build/ jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo/build$ conan install .. Configuration: [settings] arch=x86_64 arch_build=x86_64 build_type=Release compiler=gcc compiler.libcxx=libstdc++11 compiler.version=7 os=Linux os_build=Linux [options] [build_requires] [env] conanfile.txt: Installing package Requirements gtest/1.10.0 from 'conan-center' - Cache Packages gtest/1.10.0:a4062ec0208a59375ac653551e662b6cc469fe58 - Cache Installing (downloading, building) binaries... gtest/1.10.0: Already installed! conanfile.txt: Generator cmake created conanbuildinfo.cmake conanfile.txt: Generator txt created conanbuildinfo.txt conanfile.txt: Generated conaninfo.txt conanfile.txt: Generated graphinfo jed@jed-MS-7593:~/Programming/test/CPP-CMake-CodeCoverage-Demo/build$ cmake -DCMAKE_BUILD_TYPE=Debug .. -- The C compiler identification is GNU 7.5.0 -- The CXX compiler identification is GNU 7.5.0 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Conan: Adjusting output directories -- Conan: Using cmake global configuration -- Conan: Adjusting default RPATHs Conan policies -- Conan: Adjusting language standard -- Current conanbuildinfo.cmake directory: /home/jed/Programming/test/CPP-CMake-CodeCoverage-Demo/build -- Conan: Compiler GCC>=5, checking major version 7 -- Conan: Checking correct version: 7 -- Appending code coverage compiler flags: -g -fprofile-arcs -ftest-coverage -- Configuring done -- Generating done -- Build files have been written to: /home/jed/Programming/test/CPP-CMake-CodeCoverage-Demo/build jed@jed-MS-7593:~/Programming/test/CPP-CMake-CodeCoverage-Demo/build$ cmake --build . && ./bin/app Scanning dependencies of target models [ 14%] Building CXX object models/CMakeFiles/models.dir/src/employee.cpp.o [ 28%] Linking CXX shared library ../lib/libmodels.so [ 28%] Built target models Scanning dependencies of target app [ 42%] Building CXX object app/CMakeFiles/app.dir/src/main.cpp.o [ 57%] Linking CXX executable ../bin/app [ 57%] Built target app Scanning dependencies of target codecoverageexample_unittests [ 71%] Building CXX object test/CMakeFiles/codecoverageexample_unittests.dir/main.cpp.o [ 85%] Building CXX object test/CMakeFiles/codecoverageexample_unittests.dir/employee_unittest.cpp.o [100%] Linking CXX executable ../bin/codecoverageexample_unittests [100%] Built target codecoverageexample_unittests Joe Blow
Afin de prendre en charge la couverture de code, il faut l'indiquer à notre compilateur et à notre linker en configurant quelques options. Lars Bilke a créé un module CMake CodeCoverage.cmake et c'est celui que nous utiliserons.
Vous pouvez toutefois configurer manuellement les options avec entre autres -g, -fprofile-arcs et -ftest-coverage.
Le fichier CMakeLists.txt à la racine du projet contient les instructions qui activeront les options de couverture de code.
cmake_minimum_required(VERSION 3.10)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake/modules) include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) conan_basic_setup() include(CodeCoverage) append_coverage_compiler_flags() enable_testing() include(GoogleTest) add_subdirectory("models") add_subdirectory("app") add_subdirectory("test")
Allons ensuite écrire notre premier test unitaire. Ce test créer un nouvel employé qui par conséquent appelle le constructeur de la classe.
test/employee_unittest.cpp
#include "employee.h" #include <gtest/gtest.h> using namespace std; TEST(Employee_Constructor, AllValidArgs_ReturnSuccess) { Employee sample(1, "Joe", "Blow"); }
jed@jed-MS-7593:~/Programming/CPP-CMake-CodeCoverage-Demo/build$ cmake --build . \ > && ctest --progress \ > && lcov -c -d . -o main_coverage.info \ > && genhtml main_coverage.info --output-directory outSi l'on ouvre le fichier résultant index.html du dossier build\out, on voit que nos tests, ou devrais-je plutôt dire notre seul test, couvre 37.5% de notre code.
Allons voir la couverture en détail en cliquant sur le lien employee.cpp. Les lignes bleues indiquent qu'elles sont couvertes par les tests et celles en rouge indiquent qu'elles ne le sont pas.
On peut ainsi voir que nos tests ne couvrent pas le cas du constructeur ou nous passerions un id ayant la valeur zéro. Si on ajoute ce test et que relance le tout (compilation/liaison/tests/couverture de code) nous verrons que ce cas est maintenant couvert et que notre couverture code est passée de 37.5% à 43.8%.
TEST(Employee_Constructor, IdIsZero_ThrowInvalidArgument) { try { Employee sample(0, "Joe", "Blow"); FAIL(); } catch(invalid_argument &err) { ASSERT_STREQ("id must be greater than zero.", err.what()); } }
On continue, ainsi de suite jusqu'à ce que l'on atteigne une couverture de code satisfaisante.
La démo présentée ci-dessus est disponible sur mon git : https://github.com/jeremydumais/CPP-CMake-CodeCoverage-Demo