Tests¶
L'écosystème de tests C++ est mature et couvre tous les besoins : tests unitaires avec Google Test ou Catch2, tests d'intégration via CTest, mocking avec Google Mock, mesure de couverture avec gcov/lcov, et fuzzing avec AFL++ ou libFuzzer. Cette section s'appuie sur la base de code de l'API REST du chapitre 3.
Comparaison des frameworks de tests¶
| Framework | Type | Licence | Particularite |
|---|---|---|---|
| Google Test | Unitaire | BSD-3 | Le plus repandu, excellent écosystème, Google Mock intégré |
| Catch2 | Unitaire | BSL-1.0 | Header-only possible, syntaxe BDD, macros expressives |
| doctest | Unitaire | MIT | Ultra léger, intégration dans le code de production |
| CTest | Intégration/CMake | BSD | Orchestre tous les tests dans un projet CMake |
| Boost.Test | Unitaire | BSL-1.0 | Très complet, intégré à l'écosystème Boost |
Google Test — tests unitaires¶
Google Test (gtest) est le framework le plus utilisé en C++. Il s'intégré naturellement avec CMake via FetchContent ou vcpkg.
# CMakeLists.txt — integration Google Test via FetchContent
cmake_minimum_required(VERSION 3.20)
project(items_api_tests CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
enable_testing()
# Cible de tests
add_executable(tests_database
tests/test_database.cpp
src/database.cpp
)
target_include_directories(tests_database PRIVATE src)
target_link_libraries(tests_database PRIVATE GTest::gtest_main sqlite3)
include(GoogleTest)
gtest_discover_tests(tests_database)
// tests/test_database.cpp
// Tests unitaires de la couche Database avec Google Test
#include <gtest/gtest.h>
#include "database.hpp"
#include <filesystem>
// Fixture : cree une base en memoire avant chaque test, la supprime apres
class DatabaseTest : public ::testing::Test {
protected:
void SetUp() override {
// Base en memoire via le nom special SQLite ":memory:"
db_ = std::make_unique<Database>(":memory:");
}
void TearDown() override {
db_.reset();
}
std::unique_ptr<Database> db_;
};
// --- Tests de creation ---
TEST_F(DatabaseTest, CreerItemRetourneItemAvecId) {
auto item = db_->creerItem("Widget", 9.99, 100);
EXPECT_GT(item.id, 0);
EXPECT_EQ(item.nom, "Widget");
EXPECT_DOUBLE_EQ(item.prix, 9.99);
EXPECT_EQ(item.stock, 100);
}
TEST_F(DatabaseTest, CreerPlusieursItemsIncrementsIds) {
auto item1 = db_->creerItem("A", 1.0, 0);
auto item2 = db_->creerItem("B", 2.0, 0);
auto item3 = db_->creerItem("C", 3.0, 0);
EXPECT_LT(item1.id, item2.id);
EXPECT_LT(item2.id, item3.id);
}
// --- Tests de lecture ---
TEST_F(DatabaseTest, ListerItemsRetourneListeVide) {
auto items = db_->listerItems();
EXPECT_TRUE(items.empty());
}
TEST_F(DatabaseTest, ListerItemsApresCreation) {
db_->creerItem("Gadget", 5.50, 20);
db_->creerItem("Truc", 12.00, 5);
auto items = db_->listerItems();
ASSERT_EQ(items.size(), 2u);
EXPECT_EQ(items[0].nom, "Gadget");
EXPECT_EQ(items[1].nom, "Truc");
}
TEST_F(DatabaseTest, TrouverItemExistant) {
auto cree = db_->creerItem("Objet", 7.77, 3);
auto trouve = db_->trouverItem(cree.id);
ASSERT_TRUE(trouve.has_value());
EXPECT_EQ(trouve->nom, "Objet");
EXPECT_DOUBLE_EQ(trouve->prix, 7.77);
}
TEST_F(DatabaseTest, TrouverItemInexistantRetourneNullopt) {
auto resultat = db_->trouverItem(9999);
EXPECT_FALSE(resultat.has_value());
}
// --- Tests de mise a jour ---
TEST_F(DatabaseTest, MettreAJourItemExistant) {
auto item = db_->creerItem("Ancien nom", 1.0, 0);
item.nom = "Nouveau nom";
item.prix = 99.99;
item.stock = 50;
bool succes = db_->mettreAJourItem(item);
EXPECT_TRUE(succes);
auto recharge = db_->trouverItem(item.id);
ASSERT_TRUE(recharge.has_value());
EXPECT_EQ(recharge->nom, "Nouveau nom");
EXPECT_DOUBLE_EQ(recharge->prix, 99.99);
EXPECT_EQ(recharge->stock, 50);
}
TEST_F(DatabaseTest, MettreAJourItemInexistantRetourneFaux) {
Item fantome{9999, "Fantome", 0.0, 0};
bool succes = db_->mettreAJourItem(fantome);
EXPECT_FALSE(succes);
}
// --- Tests de suppression ---
TEST_F(DatabaseTest, SupprimerItemExistant) {
auto item = db_->creerItem("A supprimer", 1.0, 0);
bool succes = db_->supprimerItem(item.id);
EXPECT_TRUE(succes);
EXPECT_FALSE(db_->trouverItem(item.id).has_value());
}
TEST_F(DatabaseTest, SupprimerItemInexistantRetourneFaux) {
bool succes = db_->supprimerItem(9999);
EXPECT_FALSE(succes);
}
Google Mock — mocking des dépendances¶
Google Mock (gmock) permet de créer des objets simulacres (mocks) en remplacant des interfaces réelles par des implémentations controlees dans les tests.
// tests/test_handlers_mock.cpp
// Mock de la couche Database pour tester les handlers HTTP sans SQLite
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "database.hpp"
#include <optional>
#include <vector>
using ::testing::Return;
using ::testing::_;
// Interface abstraite — permet le mock et l'injection de dependance
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual std::vector<Item> listerItems() const = 0;
virtual std::optional<Item> trouverItem(int id) const = 0;
virtual Item creerItem(const std::string&,
double, int) = 0;
virtual bool mettreAJourItem(const Item&) = 0;
virtual bool supprimerItem(int id) = 0;
};
// Mock genere par Google Mock — MOCK_METHOD remplace chaque methode virtuelle
class MockDatabase : public IDatabase {
public:
MOCK_METHOD(std::vector<Item>, listerItems, (), (const, override));
MOCK_METHOD(std::optional<Item>, trouverItem, (int), (const, override));
MOCK_METHOD(Item, creerItem, (const std::string&, double, int), (override));
MOCK_METHOD(bool, mettreAJourItem,(const Item&), (override));
MOCK_METHOD(bool, supprimerItem, (int), (override));
};
// Test : listerItems retourne une liste de deux items
TEST(HandlersTest, ListerItemsRetourneListeCorrectement) {
MockDatabase mock;
// Programmation du comportement attendu
std::vector<Item> items_attendus = {{1, "A", 1.0, 10}, {2, "B", 2.0, 5}};
EXPECT_CALL(mock, listerItems())
.Times(1)
.WillOnce(Return(items_attendus));
// Verification du comportement
auto resultat = mock.listerItems();
ASSERT_EQ(resultat.size(), 2u);
EXPECT_EQ(resultat[0].nom, "A");
EXPECT_EQ(resultat[1].nom, "B");
}
// Test : trouverItem avec un ID inexistant retourne nullopt
TEST(HandlersTest, TrouverItemInexistantRetourneNullopt) {
MockDatabase mock;
EXPECT_CALL(mock, trouverItem(42))
.WillOnce(Return(std::nullopt));
auto r = mock.trouverItem(42);
EXPECT_FALSE(r.has_value());
}
Catch2 — syntaxe alternative¶
Catch2 offre une syntaxe plus expressive et supporte le style BDD (Given/When/Then).
// tests/test_calcul_catch2.cpp
// Tests de la bibliotheque de calcul avec Catch2 v3
// Compilation : g++ -std=c++17 test_calcul.cpp -lCatch2Main -lCatch2 -lcalcul
#define CATCH_CONFIG_MAIN
#include <catch2/catch_all.hpp>
#include "calcul.h"
#include <cmath>
#include <vector>
// Section classique Catch2
TEST_CASE("Produit scalaire", "[calcul][scalaire]") {
SECTION("Vecteurs orthogonaux — produit nul") {
std::vector<double> a = {1.0, 0.0};
std::vector<double> b = {0.0, 1.0};
REQUIRE(calcul_produit_scalaire(a.data(), b.data(), 2) == Approx(0.0));
}
SECTION("Vecteurs collineaires") {
std::vector<double> a = {1.0, 2.0, 3.0};
std::vector<double> b = {4.0, 5.0, 6.0};
REQUIRE(calcul_produit_scalaire(a.data(), b.data(), 3) == Approx(32.0));
}
}
// Style BDD avec SCENARIO/GIVEN/WHEN/THEN
SCENARIO("Calcul de norme", "[calcul][norme]") {
GIVEN("Un vecteur unitaire") {
std::vector<double> v = {1.0, 0.0, 0.0};
WHEN("On calcule sa norme") {
double n = calcul_norme(v.data(), 3);
THEN("La norme vaut 1") {
REQUIRE(n == Approx(1.0));
}
}
}
GIVEN("Un vecteur (3, 4)") {
std::vector<double> v = {3.0, 4.0};
WHEN("On calcule sa norme") {
double n = calcul_norme(v.data(), 2);
THEN("La norme vaut 5 (theoreme de Pythagore)") {
REQUIRE(n == Approx(5.0));
}
}
}
}
CTest — intégration CMake¶
CTest est l'orchestrateur de tests intégré a CMake. Il permet de lancer tous les tests du projet avec une seule commande.
# Lancement complet des tests
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failure --parallel 4
# Filtrage par nom
ctest --test-dir build -R "Database"
# Affichage verbose
ctest --test-dir build -V
Couverture de code — gcov, lcov et llvm-cov¶
# Compilation avec instrumentation gcov (GCC)
cmake -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="--coverage" \
-DCMAKE_EXE_LINKER_FLAGS="--coverage"
cmake --build build
# Execution des tests
ctest --test-dir build
# Collecte des donnees gcov
cd build
gcov -r src/database.cpp
# Rapport HTML avec lcov
lcov --capture --directory . --output-file coverage.info \
--exclude '/usr/*' --exclude '*/tests/*'
genhtml coverage.info --output-directory coverage_html
# Ouvrir coverage_html/index.html dans un navigateur
# Alternative avec llvm-cov (Clang) — rapport plus moderne
cmake -B build \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping"
cmake --build build && ctest --test-dir build
# Fusion des profils
llvm-profdata merge -sparse build/default.profraw -o coverage.profdata
# Rapport texte ou HTML
llvm-cov report build/tests_database -instr-profile=coverage.profdata
llvm-cov show build/tests_database -instr-profile=coverage.profdata \
--format=html -output-dir=coverage_html
Fuzzing — AFL++ et libFuzzer¶
Le fuzzing généré automatiquement des entrees aléatoires pour trouver des crashes et des comportements indefinis.
// fuzz/fuzz_json_parser.cpp — cible libFuzzer
// Compile avec Clang uniquement : clang++ -std=c++17 -fsanitize=fuzzer,address
// fuzz_json_parser.cpp -o fuzz_json -I../third_party
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstddef>
#include <string>
// LLVMFuzzerTestOneInput est le point d'entree de libFuzzer
// Il est appele repetitivement avec des donnees generees automatiquement
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
std::string input(reinterpret_cast<const char*>(data), size);
try {
// Tenter de parser le JSON aleatoire — ne doit jamais crasher
auto j = nlohmann::json::parse(input, nullptr, /*throw_exceptions=*/false);
if (j.is_object() && j.contains("nom")) {
// Tester les chemins de code supplementaires
(void)j["nom"].get<std::string>();
}
} catch (...) {
// Toute exception doit etre capturee — un crash non-attrape = bug
}
return 0; // 0 = entree valide pour libFuzzer
}
# Compilation et lancement libFuzzer
clang++ -std=c++17 -fsanitize=fuzzer,address -O1 \
fuzz/fuzz_json_parser.cpp -o fuzz_json -I third_party
mkdir -p corpus
./fuzz_json -max_total_time=60 corpus/ # Fuzz pendant 60 secondes
# AFL++ — necessite la compilation instrumentee avec afl-c++
afl-c++ -std=c++17 -O1 -I third_party \
fuzz/fuzz_json_parser.cpp -o fuzz_afl
mkdir -p afl_input afl_output
echo '{"nom":"test"}' > afl_input/seed.json
afl-fuzz -i afl_input -o afl_output -- ./fuzz_afl @@
Fuzzing et AddressSanitizer
Toujours combiner le fuzzing avec AddressSanitizer (-fsanitize=address) et UndefinedBehaviorSanitizer (-fsanitize=undefined). Sans les sanitizers, un buffer overflow peut passer inaperu car il ne cause pas toujours de crash immédiat. Les sanitizers detectent les erreurs mémoire au moment précis ou elles se produisent.