Aller au contenu

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.