Aller au contenu
Bienvenue sur la nouvelle documentation de CppUtils !

Les tests unitaires constituent le socle de toute base de code pérenne et évolutive. Pourtant, dans l’écosystème C++, leur mise en place impose historiquement de recourir à des bibliothèques tierces conçues il y a plus de quinze ans (GoogleTest, Catch2, doctest, Boost.Test, CppUnit). Bien qu’éprouvées, ces solutions historiques souffrent de compromis techniques majeurs :

  • Prolifération des macros préprocesseur : Les frameworks traditionnels imposent des macros aussi bien pour déclarer les suites et les cas de tests (TEST, TEST_F, TEST_CASE) que pour formuler chaque vérification (ASSERT_EQ, CHECK, etc.). Ces macros ignorent le typage statique et les namespaces, polluent le namespace global et entravent le débogage pas-à-pas. Pourtant, le C++ permet d’assurer l’enregistrement des suites de tests et le diagnostic des valeurs d’assertion sans aucun artifice du préprocesseur.
  • Absence de modularité native : Conçus pour le modèle historique d’inclusion d’en-têtes (#include), ces outils n’offrent pas d’intégration native avec les modules C++20, ce qui alourdit les temps de compilation.
  • Absence d’ordonnancement par dépendances et avalanche d’échecs : Les frameworks traditionnels exécutent les tests sans aucune notion de dépendance réelle entre les composants. Si une brique fondamentale dysfonctionne (allocateur mémoire, socket réseau, file d’attente), tous les tests dépendants s’effondrent simultanément, noyant le terminal sous des dizaines d’échecs secondaires qui masquent la cause première.
  • L’illusion du mock comme palliatif : Pour tenter d’isoler chaque composant, les développeurs multiplient les interfaces virtuelles et les objets factices (mocks). Ces simulations artificielles altèrent le comportement réel du système, alourdissent le code de production par des indirections inutiles (vtable, smart-pointers, allocations) et masquent les régressions d’intégration, les race conditions et les cas limites réels.

Le module CppUtils::UnitTest repense intégralement les tests unitaires pour l’ère moderne de C++26 :

Zéro macro préprocesseur

Syntaxe déclarative pure sans macro préprocesseur. Aucune injection de “magie” via la génération de code obscur par des macros.

Graphe de dépendances

Ordonnancement déterministe par un graphe orienté acyclique (DAG). L’exécution des tests se fait dans l’ordre de leurs dépendances, garantissant l’isolation de la cause racine sans recourir aux mocks.

Diagnostic des valeurs

Testez vos conditions avec les opérateurs naturels du langage (==, !=, <, etc.). En cas d’échec, les valeurs exactes comparées s’affichent automatiquement dans le rapport sans passer par des macros dédiées.

Isolation par processus

Chaque test s’exécute dans son propre sous-processus isolé (fork()). Résilience absolue face aux crashs et aux boucles infinies (via watchdog temporel strict avec SIGKILL).

Surveillance d'exceptions

Vérifier qu’une exception précise survient bien dans certaines situations (via throws). Ou par défaut, s’assurer qu’aucune n’est produite (noThrow). Et ce, directement dans le test, mais également dans d’autres threads créés depuis ce test.

Paramétrage de types et valeurs

Exécuter un même scénario sur un éventail de types (forTypes) ou une séquence de valeurs (forValues), que ce soit au niveau d’un test individuel ou à l’échelle d’une suite complète.

Désactivation conditionnelle de tests

Ignorer proprement des tests lorsque l’environnement cible (système d’exploitation, ressources réseau, matériel spécifique) ne remplit pas les prérequis requis (via skip et skipIf), sans faire échouer la suite.

Fuzzing déterministe

Génération pseudo-aléatoire déterministe pour tester des fonctions avec des entrées inattendues, pour pousser les cas testés dans leurs retranchements. Supporte également les chaînes de caractères aléatoires suivant des alphabets précis.

Benchmarks intégrés

Mesures de performances intégrées au framework, avec warnings et suivi comparatif si la fonctionnalité testée n’atteint pas les standards de performance.

Assertions asynchrones

Vérification périodique d’une condition (eventually) ou attente d’une opération asynchrone (completes).

Tests constexpr à la compilation

Validez vos contrats et calculs constexpr dès la compilation (suite.test<Predicate>). Détection d’erreurs instantanée au build sans aucun surcoût d’exécution au runtime.

Rapports universels CI/CD

Génération de rapports standardisés JUnit XML (--report). Intégration dans GitLab CI et GitHub Actions pour publier automatiquement les résultats et annotations dans vos pull requests.


CppUtils::UnitTest rompt avec les compromis des frameworks historiques : aucune macro préprocesseur, capture directe des expressions et ordonnancement automatique des suites par un graphe de dépendances.

Le comparatif interactif ci-dessous illustre un scénario de test identique exprimé dans chaque framework :

// Support natif des modules C++20 (zéro macro, zéro fichier d'en-tête)
import std;
import CppUtils;
namespace MyApp::Tests
{
namespace ut = CppUtils::UnitTest;
// Déclaration de la suite avec dépendances
auto _ = ut::testSuite("Math/Calculator").dependsOn("Logger") = [](ut::TestSuite& suite) {
suite.test("Operations") = [] {
// Assertions avec capture des valeurs réelles sans macro
ut::expect(add(20, 22)) == 42;
not ut::expect(isNegative(42));
ut::expect([&] { divide(10, 0); }) == ut::throws<std::invalid_argument>;
};
// Test paramétré sur une collection de valeurs
suite.test("Scale factor").forValues(10, 50, 100) = [](int scale) {
ut::expect(scale) > 0;
};
};
}
FonctionnalitéCppUtils::UnitTestBoost.UTCatch2 (v3)Google TestdoctestBoost.TestQt TestMS Test (C++)CppUnit
Zéro macro de préprocesseur✅✅❌ (REQUIRE)❌ (EXPECT_EQ)❌ (CHECK)❌ (BOOST_CHECK)❌ (QCOMPARE)❌ (TEST_METHOD)❌ (CPPUNIT_)
Support natif des modules C++20✅ (import)✅ (ut.cppm)❌ (en-têtes #include)❌ (en-têtes #include)❌ (en-têtes #include)❌ (en-têtes #include)❌ (en-têtes #include)❌ (en-têtes #include)❌ (en-têtes #include)
Décomposition d’expressions (lhs == rhs)✅ (syntaxe expect(a) == b)✅ (1_i == 2 ou that % / eq)⚠️ Partiel (décomposition dans macros)❌ (macros dédiées ASSERT_EQ)⚠️ Partiel (décomposition dans macros)⚠️ Partiel (via macro BOOST_TEST)❌ (QCOMPARE)❌ (Assert::AreEqual)❌ (CPPUNIT_ASSERT_EQUAL)
Mock❌ (découvrir pourquoi)❌❌ (via tiers)✅ (GoogleMock)❌ (via tiers)❌ (via tiers)⚠️ Partiel (QSignalSpy)❌❌
Ordonnancement par dépendances✅ (ordonnancement topologique et arrêt des branches défaillantes)❌❌❌❌❌❌❌❌
Benchmarks intégrés et comparatifs✅ (benchmark, warnIfSlower)❌⚠️ Partiel (BENCHMARK)❌❌❌❌❌❌
Tests paramétrés (forValues)✅✅✅ (GENERATE)✅ (INSTANTIATE_TEST_SUITE_P)✅ (GENERATE)✅ (datasets)⚠️ Partiel (QTest::newRow)❌❌
Tests typés (forTypes)✅ (direct via variadique)✅✅ (TEMPLATE_TEST_CASE)✅ (TYPED_TEST)⚠️ Partiel (TYPE_TO_STRING)✅ (template test cases)❌❌❌
Moteur de fuzzing déterministe✅ (avec seed reproductible)❌❌❌❌❌❌❌❌
Isolation par processus (fork)✅ (résilience SIGSEGV)❌❌⚠️ Partiel (death_test)❌❌❌❌❌
Vérification asynchrone (eventually, completes)✅ (polling périodique et réveil réactif)❌❌❌❌❌❌❌❌
Rapports JUnit XML (CI/CD)✅ (natif avec --report=junit:...)❌✅ (-r junit)✅ (--gtest_output=xml)✅ (-r=junit)✅ (--log_format=JUNIT)✅ (-junitxml)⚠️ Partiel (.trx propriétaire, JUnit via plugin tiers)✅ (XmlOutputter)
Zéro dépendance externe✅✅✅✅✅❌ (Boost)❌ (Qt Core & MOC)❌ (Visual Studio)✅
  • Clarté du code et outillage moderne : L’absence totale de macros permet une autocomplétion parfaite, un refactoring fiable dans votre éditeur de code, et un débogage pas-à-pas transparent sans code généré invisible.
  • Positionnement vis-à-vis des alternatives modernes : Parmi l’ensemble des frameworks existants, Boost.UT constitue l’alternative la plus avancée et respectable pour s’affranchir des macros. CppUtils::UnitTest en conserve l’esprit moderne tout en éliminant les contraintes restantes : aucune exigence de littéraux dédiés (_i) pour capturer n’importe quel type standard, ordonnancement par graphe de dépendances et résilience par isolation étanche des processus.
  • Résolution immédiate de la cause racine : Grâce au graphe de dépendances (CppUtils::Container::DependencyGraph), si un module fondamental de votre application échoue, les centaines de tests dépendants sont automatiquement ignorés au lieu d’inonder la console d’erreurs parasites.
  • Tout-en-un de la qualité logicielle : Au lieu de jongler entre Google Test pour les tests unitaires, Google Benchmark pour les performances, libFuzzer pour la robustesse et des scripts maison pour l’isolation des processus, CppUtils::UnitTest rassemble ces piliers dans une interface cohérente et expressive.

Une suite de tests se déclare simplement :

import std;
import CppUtils;
namespace MyApp::Tests
{
namespace ut = CppUtils::UnitTest;
auto _ = ut::testSuite("Math/Calculator") = [](ut::TestSuite& suite) {
using namespace std::literals;
suite.test("Addition") = [] {
const auto result = 20 + 22;
ut::expect(result) == 42;
};
suite.test("Calcul intensif").timeout(30s) = [] {
ut::expect(performHeavyComputation()) > 0;
};
};
}

Lorsqu’une suite nécessite que d’autres suites de tests soient préalablement validées avant de s’exécuter, déclarez-les avec .dependsOn(...) :

auto _ = ut::testSuite("Network/Client").dependsOn("Logger", "Network/Socket") = [](ut::TestSuite& suite) {
suite.test("Connection") = [] {
// Ce test ne s'exécutera que si "Logger" et "Network/Socket" ont réussi
};
};
flowchart TD
    subgraph Prerequisites["Suites prérequises"]
        Logger["Logger"]
        Socket["Network/Socket"]
    end

    subgraph Dependent["Suite dépendante"]
        Client["Network/Client"]
    end

    Logger -->|"Succès requis"| Client
    Socket -->|"Succès requis"| Client

En déclarant ces dépendances, l’exécuteur structure l’ordonnancement sous la forme d’un graphe orienté acyclique (DAG). Cette organisation apporte trois bénéfices majeurs :

  • Localisation immédiate de la cause racine d’un échec : Si la suite Network/Socket échoue, la suite Network/Client n’est même pas lancée et est marquée comme ignorée en raison d’un prérequis non satisfait. Vous évitez ainsi de voir la cause réelle noyée sous une avalanche d’échecs en cascade provoqués par une dépendance défaillante.
  • Sortie console épurée avec le mode failFast : Lorsque l’option failFast est activée, l’exécuteur s’interrompt dès le premier échec sans exécuter les suites dépendantes.
  • Suppression du besoin de mocking : Puisque les tests s’exécutent dans l’ordre strict de leurs dépendances réelles et que les pannes sont isolées à la source, il devient inutile d’introduire des doublures de test artificielles pour masquer les instabilités en amont. Vous pouvez tester directement vos composants réels de bout en bout en toute confiance.

L’absence d’utilitaire de mock au sein de CppUtils est un choix délibéré. Dans l’écosystème C++, le recours aux mocks pose deux problèmes majeurs :

1. L’illusion du test et le piège des fausses garanties

Section intitulée « 1. L’illusion du test et le piège des fausses garanties »

Un mock remplace un composant authentique par des réponses pré-programmées. Ce procédé donne l’illusion de valider le composant, alors que l’interaction réelle n’a jamais été exécutée :

  • Exemple réseau : Si l’on mocke une socket pour lui faire renvoyer un JSON figé, on valide le parsing dans des conditions idéales, mais on ignore comment notre composant réagit aux dynamiques réelles du flux : paquets fragmentés, désynchronisation d’état ou race conditions si des données arrivent pendant un changement d’état interne. Ces deux traitements peuvent alors entrer en conflit et corrompre le fonctionnement du composant, un défaut indétectable avec un mock.
  • Exemple filesystem : Si l’on mocke le filesystem, on ignore les verrous concurrents, les droits d’accès réels du système d’exploitation et la synchronisation sur disque.

En conditions réelles, le système se retrouve donc dans une situation qui n’a véritablement jamais été testée. CppUtils préconise de tester les interfaces en conditions authentiques : démarrer un vrai serveur local sur un port éphémère ou manipuler de vrais fichiers temporaires.

2. Dénaturation du code de production (Zero-cost abstraction)

Section intitulée « 2. Dénaturation du code de production (Zero-cost abstraction) »

Pour rendre une classe « mockable », le développeur est souvent contraint de dénaturer son architecture : imposer des hiérarchies orientées objet (POO) pour l’injection de dépendances, multiplier les interfaces virtuelles (virtual / vtable) et recourir aux allocations dynamiques (std::unique_ptr<Interface>). Ces indirections empêchent l’inlining par le compilateur et alourdissent le code de production uniquement pour satisfaire le banc de tests.

Le besoin de mocks naît souvent de la peur qu’une panne en amont ne fasse échouer tous les tests en cascade et qu’on ne sache plus isoler facilement la cause racine du problème. Grâce à CppUtils::Container::DependencyGraph, CppUtils résout ce problème : les briques de base sont exécutées en premier. Si une fondation échoue, les suites dépendantes sont automatiquement ignorées, isolant immédiatement la cause première sans introduire de simulation artificielle.


Contrairement aux frameworks traditionnels qui imposent de mémoriser des dizaines de macros spécialisées selon le type de vérification (ASSERT_EQ, ASSERT_NE, ASSERT_THROW, CHECK_FALSE, etc. (voir Comparatif)), CppUtils::UnitTest privilégie la simplicité en ne proposant qu’une seule fonction d’assertion : expect(...).

La fonction expect valide qu’une condition ou une expression est vraie :

ut::expect(database.isConnected());
ut::expect(not std::ranges::empty(buffer));

Écrire == true est inutile : expect(condition); évalue directement la condition.

En cas d’échec, le rapport affiche précisément le fichier, la ligne, la colonne ainsi que la valeur textuelle défaillante :

Terminal
──────────────────────────────────────────────────────────────
Database/Connection/Ping:
                         🛇 ERROR 🛇
The following test didn't pass:
Database/Connection/Ping
  In tests/Database/Connection.mpp
At line 42, column 5
In expect(condition)
with value:
false

Failed ✕

Tout type convertible en booléen (std::optional, pointeurs bruts, std::unique_ptr, std::shared_ptr, types avec explicit operator bool()) est automatiquement converti et évalué sous forme de booléen par expect :

auto user = std::optional<User>{fetchUser(42)};
ut::expect(user);
auto connection = std::unique_ptr<Socket>{connect()};
ut::expect(connection);

En cas d’échec sur un pointeur nul ou un std::optional vide, le rapport affiche directement with value: nullptr ou with value: nullopt en rouge :

Terminal
──────────────────────────────────────────────────────────────
Database/Connection/Open:
                         🛇 ERROR 🛇
The following test didn't pass:
Database/Connection/Open
  In tests/Database/Connection.mpp
At line 42, column 2
In expect(condition)
with value:
nullopt

Failed ✕

Négation : not expect(...) plutôt que expect(not ...)

Section intitulée « Négation : not expect(...) plutôt que expect(not ...) »

Pour affirmer qu’une condition doit être fausse, préférez placer la négation à l’extérieur :

// Recommandé :
not expect(session.isExpired());
// Équivalent :
expect(session.isExpired()) == false;

Bien qu’écrire expect(not condition); fonctionne en C++, la négation externe offre un diagnostic bien plus riche en cas d’échec :

  • Avec expect(not error); : la négation logique est résolue avant l’appel à expect. L’expression est immédiatement réduite à un simple bool. En cas d’échec, le rapport affiche uniquement with value: false sans révéler le contenu de l’objet :
auto error = std::optional<std::string>{"Connexion refusée"};
expect(not error);
Terminal
──────────────────────────────────────────────────────────────
Network/Connection:
                         🛇 ERROR 🛇
The following test didn't pass:
Network/Connection
  In tests/Network/Connection.mpp
At line 42, column 5
In expect(condition)
with value:
false

Failed ✕
  • Avec not expect(error); : le framework capture l’objet d’origine intact. Pour tout objet déréférençable (operator*) dont la valeur sous-jacente est affichable, le diagnostic extrait et affiche directement l’élément inattendu qui a provoqué l’erreur :
auto error = std::optional<std::string>{"Connexion refusée"};
not expect(error);
Terminal
──────────────────────────────────────────────────────────────
Network/Connection:
                         🛇 ERROR 🛇
The following test didn't pass:
Network/Connection
  In tests/Network/Connection.mpp
At line 42, column 5
In not expect(condition)
with value:
Connexion refusée

Failed ✕

Dans les bibliothèques traditionnelles, inspecter la valeur des opérandes impose de renoncer aux opérateurs C++ au profit d’une longue liste de macros spécialisées à mémoriser (ASSERT_EQ, ASSERT_NE, ASSERT_LT, ASSERT_LE, ASSERT_GT, ASSERT_GE, EXPECT_FLOAT_EQ, etc.).

Avec CppUtils::UnitTest, les opérateurs standards du C++ remplacent l’intégralité de ces macros, sans wrapper verbeux ni littéral propriétaire :

ut::expect(left) == right;
ut::expect(left) != right;
ut::expect(left) < right;
ut::expect(left) <= right;
ut::expect(left) > right;
ut::expect(left) >= right;
namespace ut = CppUtils::UnitTest;
auto _ = ut::testSuite("Auth/Session") = [](ut::TestSuite& suite) {
using namespace std::chrono_literals;
suite.test("Validate session") = [] {
ut::expect(session.getRemainingTime()) > 0s;
ut::expect(session.getUserRole()) == "admin";
ut::expect(session.getFailedAttempts()) <= 3u;
ut::expect(std::ranges::size(session.getActiveTokens())) == 2uz;
};
};
  1. Pur C++23 sans macro préprocesseur : Dans des frameworks comme Catch2 ou GoogleTest, la décomposition d’expression nécessite impérativement une macro (REQUIRE(a == b), EXPECT_EQ(a, b)). En C++ pur, écrire expect(a == b); transmettrait un simple bool déjà évalué à expect, rendant impossible l’inspection séparée de a et b. En écrivant expect(a) == b;, expect(a) renvoie un objet proxy temporaire (ExpectationProxy) qui intercepte l’opérateur de comparaison suivant et capture à la fois la valeur de gauche (lhs) et celle de droite (rhs) avec leur opérateur sans la moindre macro.
  2. Zéro conflit d’identifiants et zéro masquage de variable : Les approches basées sur des fonctions libres comme value(...) ou that(...) entraînent des conflits majeurs dès qu’un test déclare une variable locale portant ce même nom usuel. Écrire auto value = calculate(); expect(value(value) == 42); produit une erreur fatale à la compilation (error: type does not provide a call operator). Avec expect(value) == 42;, la variable value est passée directement sans aucun risque d’écrasement ou d’ambiguïté.
  3. Zéro pollution par des littéraux de test personnalisés : Pour intercepter les comparaisons sans macro, Boost.UT doit forcer au moins l’un des opérandes dans un type intermédiaire : soit via des suffixes littéraux propriétaires (42_i == sum()), restreints aux types arithmétiques primitifs, soit via un wrapper de matcher (expect(that % 0 == sum());), soit via des fonctions préfixées (expect(eq(a, b));). CppUtils::UnitTest résout cette problématique de manière unifiée et générique sans aucun compromis : aucun littéral utilisateur imposé, aucun wrapper verbeux et aucune fonction préfixée. N’importe quel type C++ moderne est immédiatement capturé avec les opérateurs naturels du langage.
  4. Lisibilité directe : L’assertion se lit de gauche à droite sans rupture (“expect left equals right”) et sans préfixes redondants.
  5. Assertions sans opérateur relationnel : Les conditions booléennes s’évaluent directement (par exemple ut::expect(user.isConnected());) sans exiger d’opérateur de comparaison ni de suffixe == true.

En cas d’échec (par exemple si user.getRole() renvoie "guest" au lieu de "admin"), le diagnostic décompose l’expression et affiche les valeurs exactes des deux opérandes :

Terminal
──────────────────────────────────────────────────────────────
UnitTest/Auth:
                         🛇 ERROR 🛇
The following test didn't pass:
Auth/Session/Admin verification
  In tests/Auth/Session.mpp
At line 42, column 2
In expect(lhs) == rhs
with lhs:
guest
and rhs:
admin

Diff:
  lhs (-): "guest"
  rhs (+): "admin"

Failed ✕

Lors de divergences ponctuelles au sein de longues chaînes, ce calcul de diff isole instantanément les caractères fautifs au milieu du texte identique :

const auto expectedUrl = "https://api.example.com/v1/users/42/profile";
const auto actualUrl = "https://api.example.com/v2/users/42/profile";
ut::expect(expectedUrl) == actualUrl;
Terminal
──────────────────────────────────────────────────────────────
Network/Api/EndpointCheck:
                         🛇 ERROR 🛇
The following test didn't pass:
Network/Api/EndpointCheck
  In tests/Network/Api.mpp
At line 18, column 2
In expect(lhs) == rhs
with lhs:
https://api.example.com/v1/users/42/profile
and rhs:
https://api.example.com/v2/users/42/profile

Diff:
  lhs (-): "https://api.example.com/v1/users/42/profile"
  rhs (+): "https://api.example.com/v2/users/42/profile"

Failed ✕

(Comme affiché ci-dessus, seul le chiffre 1 apparaît en rouge et le 2 en vert, tout le reste de l’URL restant en couleur neutre).

De même, lorsque plusieurs segments diffèrent au sein d’une même phrase, chaque divergence est mise en évidence individuellement sans amalgamer le texte intermédiaire inchangé :

const auto expectedSentence = "The quick brown fox jumps over the lazy dog";
const auto actualSentence = "The fast brown fox leaps over a lazy dog";
ut::expect(expectedSentence) == actualSentence;
Terminal
──────────────────────────────────────────────────────────────
UnitTest/VisualColorInspection/Diff visuel complexe (segments multiples):
                         🛇 ERROR 🛇
The following test didn't pass:
UnitTest/VisualColorInspection/Diff visuel complexe (segments multiples)
  In tests/UnitTest/UnitTest.mpp
At line 711, column 5
In expect(lhs) == rhs
with lhs:
The quick brown fox jumps over the lazy dog
and rhs:
The fast brown fox leaps over a lazy dog

Diff:
  lhs (-): "The quick brown fox jumps over the lazy dog"
  rhs (+): "The fast brown fox leaps over a lazy dog"

Failed ✕

Le moteur d’inspection s’appuie sur des concepts C++20 génériques, sans exiger de macro de sérialisation ni de surcharge d’affichage (contrairement par exemple à QTest::toString dans Qt Test). Tout type qui satisfait ces concepts (plages itérables, types déréférençables, structures décomposables façon tuple, etc.), qu’il provienne de la bibliothèque standard, d’une bibliothèque tierce ou de votre propre code, est automatiquement analysé et mis en forme.

Les conteneurs (std::vector, std::span, listes, vues) sont comparés élément par élément avec std::ranges::equal et formatés sous forme de liste lisible :

const auto headers = std::vector<std::string>{"content-type", "authorization"};
ut::expect(headers) == std::vector<std::string>{"content-type", "accept"};
Terminal
──────────────────────────────────────────────────────────────
Network/Headers/Validate:
                         🛇 ERROR 🛇
The following test didn't pass:
Network/Headers/Validate
  In tests/Network/Headers.mpp
At line 32, column 2
In expect(lhs) == rhs
with lhs:
["content-type", "authorization"]
and rhs:
["content-type", "accept"]

Diff:
  lhs (-): ["content-type", "authorization"]
  rhs (+): ["content-type", "accept"]

Failed ✕

Les structures composites std::tuple et std::pair sont décomposées et formatées récursivement champ par champ :

const auto response = std::make_pair(404, "Not Found");
ut::expect(response) == std::make_pair(200, "OK");
Terminal
──────────────────────────────────────────────────────────────
Network/Response/Validate:
                         🛇 ERROR 🛇
The following test didn't pass:
Network/Response/Validate
  In tests/Network/Response.mpp
At line 15, column 2
In expect(lhs) == rhs
with lhs:
(404, "Not Found")
and rhs:
(200, "OK")

Diff:
  lhs (-): (404, "Not Found")
  rhs (+): (200, "OK")

Failed ✕

std::optional, std::unique_ptr et std::shared_ptr sont inspectés automatiquement : ils affichent explicitement nullopt ou nullptr lorsqu’ils sont vides, ou la valeur pointée déréférencée :

const auto cachedUser = std::optional<std::string>{std::nullopt};
ut::expect(cachedUser) == std::optional<std::string>{"alice"};
Terminal
──────────────────────────────────────────────────────────────
Auth/Cache/Lookup:
                         🛇 ERROR 🛇
The following test didn't pass:
Auth/Cache/Lookup
  In tests/Auth/Cache.mpp
At line 24, column 2
In expect(lhs) == rhs
with lhs:
nullopt
and rhs:
"alice"

Failed ✕

Les octets mémoire sont mis en forme sous notation hexadécimale explicite (0x2A), y compris lorsqu’ils sont combinés dans des collections (std::vector<std::byte>, std::span<const std::byte>), idéal pour tester des protocoles binaires et des en-têtes réseau :

const auto header = std::vector<std::byte>{std::byte{0x7F}, std::byte{0x45}, std::byte{0x4C}, std::byte{0x46}}; // ELF magic
ut::expect(header) == std::vector<std::byte>{std::byte{0x7F}, std::byte{0x45}, std::byte{0x00}, std::byte{0x00}};
Terminal
──────────────────────────────────────────────────────────────
Binary/Header/Magic:
                         🛇 ERROR 🛇
The following test didn't pass:
Binary/Header/Magic
  In tests/Binary/Header.mpp
At line 12, column 2
In expect(lhs) == rhs
with lhs:
[0x7F, 0x45, 0x4C, 0x46]
and rhs:
[0x7F, 0x45, 0x00, 0x00]

Diff:
  lhs (-): [0x7F, 0x45, 0x4C, 0x46]
  rhs (+): [0x7F, 0x45, 0x00, 0x00]

Failed ✕

Les chemins de fichiers sont normalisés et affichés sous leur forme canonique :

Terminal
──────────────────────────────────────────────────────────────
FileSystem/Config/PathCheck:
                         🛇 ERROR 🛇
The following test didn't pass:
FileSystem/Config/PathCheck
  In tests/FileSystem/Config.mpp
At line 28, column 2
In expect(lhs) == rhs
with lhs:
"/var/log/syslog"
and rhs:
"/var/log/app.log"

Diff:
  lhs (-): "/var/log/syslog"
  rhs (+): "/var/log/app.log"

Failed ✕

Nombres flottants avec tolérance epsilon automatique

Section intitulée « Nombres flottants avec tolérance epsilon automatique »

Les comparaisons de nombres à virgule flottante (float, double) s’appuient automatiquement sur une tolérance relative epsilon (Math::isEqual), éliminant les échecs liés aux arrondis binaires IEEE 754 sans imposer de macro dédiée (EXPECT_NEAR) :

ut::expect(0.1 + 0.2) == 0.3; // Valide automatiquement malgré l'imprécision IEEE 754

Les comparaisons mixtes s’appuient sur les fonctions sécurisées de la bibliothèque standard (std::cmp_equal, std::cmp_greater, etc.), éliminant les faux positifs dus aux conversions implicites risquées du C++ :

ut::expect(-1) > 1u; // Faux en C++ standard à cause de la conversion non signée ; évalué correctement ici

La comparaison avec un littéral textuel compare le contenu des chaînes via std::string_view, éliminant le risque classique de comparer par inadvertance les adresses mémoire des pointeurs const char*.

Matchers : throws, noThrow, crashes, eventually, completes

Section intitulée « Matchers : throws, noThrow, crashes, eventually, completes »

Plutôt que d’invoquer des fonctions d’assertion verbeuses et dispersées (ASSERT_THROW, ASSERT_NO_THROW, ASSERT_DEATH, etc.), CppUtils::UnitTest unifie les vérifications via l’opérateur == appliqué à expect(action) :

Validation du type d’exception :

ut::expect([&] {
computeSquareRoot(-1.0);
}) == ut::throws<std::invalid_argument>;

Validation que le message d’erreur contient une sous-chaîne :

ut::expect([&] {
divide(10, 0);
}) == ut::throws<std::invalid_argument>("Division par zéro");
// Valide si exception.what() contient "Division par zéro" (ex. "Erreur mathématique: Division par zéro détectée")

Validation par prédicat booléen :

ut::expect([&] {
database.connect("invalid_host");
}) == ut::throws<DatabaseException>([](const DatabaseException& error) {
return error.code == 404 and error.message.contains("unreachable");
});

Validation par comparaison directe de valeur :

ut::expect([&] {
parser.parse("!");
}) == ut::throws<ParseError>(ParseError{.line = 1, .code = ErrorCode::UnexpectedToken});

Validation par inspecteur avec assertions internes :

ut::expect([&] {
database.connect("invalid_host");
}) == ut::throws<DatabaseException>([&](const DatabaseException& error) {
ut::expect(error.code) == 404;
ut::expect(error.message.contains("unreachable"));
});

Vérifie qu’aucune exception n’est lancée lors de l’exécution :

ut::expect([&] {
socket.receive();
}) == ut::noThrow;

Matcher crashes : Interception de crashs et signaux critiques

Section intitulée « Matcher crashes : Interception de crashs et signaux critiques »

Validation d’une interruption fatale (tout signal d’arrêt critique) :

ut::expect([&] {
int* nullPointer = nullptr;
*nullPointer = 42;
}) == ut::crashes;

Validation d’un signal fatal spécifique :

ut::expect([&] {
std::abort();
}) == ut::crashes(CppUtils::System::Signal::Abort);

Matcher eventually : Attente d’une condition asynchrone

Section intitulée « Matcher eventually : Attente d’une condition asynchrone »

Vérifie périodiquement qu’une condition asynchrone devient vraie avant un délai maximal.

Par défaut, eventually attend jusqu’à 1 seconde en réévaluant la condition toutes les 20 ms :

// Utilisation avec les valeurs par défaut (timeout: 1s, step: 20ms)
ut::expect([&] {
return counter.load() == 5uz;
}) == ut::eventually;

Il est également possible de personnaliser le délai maximal seul, ou conjointement avec l’intervalle périodique :

// Délai maximal personnalisé de 500ms (intervalle conservé à 20ms)
ut::expect([&] {
return isReady.load();
}) == ut::eventually(500ms);
// Délai maximal (500ms) et intervalle d'évaluation (10ms) personnalisés
ut::expect([&] {
return counter.load() == 5uz;
}) == ut::eventually(500ms, 10ms);

Matcher completes : Synchronisation réactive sans polling

Section intitulée « Matcher completes : Synchronisation réactive sans polling »

Attend la fin d’une opération asynchrone ou le déblocage d’une primitive de synchronisation avant un délai maximal (1 seconde par défaut).

Attente d’un std::future :

auto promise = std::promise<int>{};
auto future = promise.get_future();
worker.execute([&] { promise.set_value(42); });
// Validation de la complétion seule
ut::expect(future) == ut::completes;
// Validation combinée de la complétion et de la valeur
ut::expect(future) == ut::completes.with(42);
// Personnalisation du délai maximal
ut::expect(future) == ut::completes(500ms).with(42);

Avec une lambda retournant un résultat asynchrone :

ut::expect([&] { return worker.computeAsync(); }) == ut::completes.with(42);

Avec un std::shared_future :

auto sharedFuture = promise.get_future().share();
ut::expect(sharedFuture) == ut::completes.with(42);

Avec un sémaphore C++20 (std::binary_semaphore, std::counting_semaphore) :

auto signal = std::binary_semaphore{0};
scheduler.schedule([&] { signal.release(); }, 50ms);
ut::expect(signal) == ut::completes;

Lorsqu’un test est temporairement désactivé ou requiert un environnement spécifique (système d’exploitation particulier, ressource matérielle requise, connectivité réseau distante, etc.), vous pouvez l’ignorer proprement sans faire échouer la suite :

// Ignorer inconditionnellement un test (ex. fonctionnalité en cours de développement ou bogue en attente de correctif)
skip("Fonctionnalité en cours de refactorisation (ticket #142)");
// Ignorer un test selon une condition
skipIf(not isNetworkAvailable(), "Connexion réseau distante requise");

Pour valider vos algorithmes constexpr, métaprogrammation par templates, concepts ou constantes statiques, le framework de test fournit une surcharge directe de suite.test évaluée à la compilation :

suite.test<[] {
constexpr auto value = factorial(5);
return value == 120;
}>("Factorial computation");

Ce mécanisme combine la rigueur d’un static_assert avec l’intégration complète au banc de tests :

  • Feedback statique immédiat sans réexécution : Si le prédicat échoue, la compilation échoue immédiatement avec une erreur static_assert, bloquant le binaire avant toute exécution. Vous savez que le test passe dès que le projet compile, même sans avoir à relancer la suite de tests.
  • Intégration au rapport et statistiques : Le test compile-time est comptabilisé dans le bilan de la suite et affiche son statut (Passed ✓).

Le framework intègre un moteur de fuzzing pseudo-aléatoire déterministe. Ce déterminisme garantit que chaque exécution de test rejoue rigoureusement la même séquence de données pour une reproductibilité absolue en intégration continue :

// Exécute 5 000 itérations avec des valeurs régénérées pseudo-aléatoirement à chaque passage
suite.fuzzTest("Parser fuzzing", 5'000uz) = [](auto& fuzzer) {
// Génère une chaîne pseudo-aléatoire de longueur 1 à 64 caractères
const auto input = fuzzer.randomString(1uz, 64uz);
// Valide que le parser ne subit aucun comportement indéfini ni plantage
parseData(input);
};

Vous pouvez également injecter des séquences pseudo-aléatoires au sein d’un test unitaire classique à l’aide de fuzz :

suite.test("Integer arithmetic fuzzing") = [] {
fuzz("Random arithmetic checks", 1'000uz, [](auto& fuzzer) {
const auto a = fuzzer.random(-1000, 1000);
// Borne inférieure à 1 pour exclure toute division par zéro
const auto b = fuzzer.random(1, 1000);
expect((a / b) * b + (a % b)) == a;
});
};

L’objet fuzzer (instance de CppUtils::Math::DeterministicRandomEngine) met à disposition un ensemble complet de méthodes de génération déterministes entièrement compatibles constexpr :

Méthode & signatureDescription
random<Type>()Génère une valeur pseudo-aléatoire du type demandé (int, size_t, double, bool, etc.) couvrant par défaut les bornes minimales et maximales du type.
random(min, max)Génère une valeur pseudo-aléatoire bornée dans l’intervalle [min, max] avec déduction automatique du type (entier ou flottant).
randomSize(max)Raccourci pratique pour générer une taille std::size_t comprise entre 0 et max inclus (ou dans [min, max] avec 2 arguments). Sans argument, génère une taille bornée par défaut par PTRDIFF_MAX conformément à la limite légale des objets contigus en C++.
randomFloat(min, max)Génère un flottant dans l’intervalle [min, max] (par défaut 0.0 à 1.0).
randomString(minLen, maxLen, alphabet)Génère une chaîne de longueur variable (par défaut 0 à 32 caractères) issue de l’alphabet fourni.
randomBytes(count)Alloue et remplit un vecteur de count octets (std::vector<std::byte>).
pickOne(collection)Sélectionne un élément aléatoire au sein d’une collection.

Pour le détail exhaustif de leurs paramètres et surcharges, consultez la référence d’API de DeterministicRandomEngine.

Le paramètre alphabet de randomString permet de cibler des motifs et formats spécifiques :

// Longueur fixe de 32 caractères (min=32, max=32) tirés de l'alphabet hexadécimal (hash, token UUID)
auto hexToken = fuzzer.randomString(32uz, 32uz, "0123456789abcdef");
// Longueur variable de 1 à 10 caractères restreinte aux seuls chiffres 0 à 9
auto numericId = fuzzer.randomString(1uz, 10uz, "0123456789");
// Longueur variable de 5 à 20 caractères spéciaux pour éprouver la robustesse d'un parseur
auto syntaxPayload = fuzzer.randomString(5uz, 20uz, "{}[]():;\"'\\/<>&=");

Le framework met à disposition des utilitaires légers pour chronométrer votre code et comparer directement les performances de deux implémentations :

suite.test("FlatHashMap vs std::unordered_map performance") = [] {
constexpr auto elementCount = 50'000uz;
auto flatMap = CppUtils::Container::FlatHashMap<std::size_t, std::size_t>{};
auto stdMap = std::unordered_map<std::size_t, std::size_t>{};
const auto flatBenchmark = benchmark("FlatHashMap insert", [&] {
for (auto index = 0uz; index < elementCount; ++index)
flatMap[index] = index * 2uz;
});
const auto stdMapBenchmark = benchmark("std::unordered_map insert", [&] {
for (auto index = 0uz; index < elementCount; ++index)
stdMap[index] = index * 2uz;
});
// Compare les deux benchmarks et rapporte le facteur d'accélération
const auto comparison = compareBenchmarks(flatBenchmark, stdMapBenchmark);
// Avertissement non bloquant : affiche une alerte dans la console si le candidat est plus lent
warnIfSlower(flatBenchmark, stdMapBenchmark);
// Si vous souhaitez au contraire imposer une contrainte stricte qui fait échouer le test :
// expect(comparison.isFirstFaster());
};

Tests paramétrés par types et par valeurs (forTypes et forValues)

Section intitulée « Tests paramétrés par types et par valeurs (forTypes et forValues) »

Le framework de test prend en charge l’exécution itérative d’un même scénario de test pour une liste de types ou un jeu de valeurs distinctes. Ces modificateurs s’appliquent aussi bien au niveau d’un test unitaire isolé qu’au niveau d’une suite entière.

Le corps du test est instancié et enregistré comme autant de tests distincts (nommés automatiquement avec le suffixe [type 0], [type 1], etc.) pour chacun des types spécifiés en argument template :

suite.test("Vector operations").forTypes<int, double, std::string>() = []<class Type> {
auto vector = std::vector<Type>{};
vector.push_back(Type{});
};

Rapport généré dans la console lors de l’exécution :

Terminal
──────────────────────────────────────────────────────────────
Container/Vector/Vector operations [type 0]:
Passed ✓
──────────────────────────────────────────────────────────────
Container/Vector/Vector operations [type 1]:
Passed ✓
──────────────────────────────────────────────────────────────
Container/Vector/Vector operations [type 2]:
Passed ✓

Le test génère une entrée distincte pour chaque valeur transmise. Si le type est formatable avec std::format, la valeur elle-même apparaît entre crochets dans l’intitulé du test ([10], [50], etc.) :

suite.test("Positive scale factor").forValues(10, 50, 100, 500) = [](int scale) {
expect(scale) > 0;
};

Rapport généré dans la console lors de l’exécution :

Terminal
──────────────────────────────────────────────────────────────
Math/Scale/Positive scale factor [10]:
Passed ✓
──────────────────────────────────────────────────────────────
Math/Scale/Positive scale factor [50]:
Passed ✓
──────────────────────────────────────────────────────────────
Math/Scale/Positive scale factor [100]:
Passed ✓
──────────────────────────────────────────────────────────────
Math/Scale/Positive scale factor [500]:
Passed ✓

Itération sur un conteneur ou une plage (std::ranges)

Section intitulée « Itération sur un conteneur ou une plage (std::ranges) »

Vous pouvez également transmettre un conteneur standard ou n’importe quelle vue std::ranges (comme std::views::iota pour tester une séquence numérique paresseuse sans aucune allocation dynamique) ; chaque élément génère alors un cas de test individuel :

// Séquence paresseuse avec std::views::iota (sans allocation mémoire) :
suite.test("Index validation").forValues(std::views::iota(1, 10)) = [](int index) {
expect(index) >= 1;
expect(index) < 10;
};
// Conteneur séquentiel standard :
suite.test("Buffer allocation").forValues(std::vector{64uz, 256uz, 1024uz}) = [](std::size_t size) {
auto buffer = std::vector<std::uint8_t>(size, 0);
expect(std::ranges::size(buffer)) == size;
};
// Conteneur associatif (itère sur chaque paire std::pair clé-valeur) :
const auto rolePermissions = std::map<std::string, int>{
{"guest", 1},
{"user", 2},
{"admin", 4}
};
suite.test("Role permissions").forValues(rolePermissions) = [](const auto& entry) {
const auto& [role, permissionLevel] = entry;
expect(permissionLevel) > 0;
};

Le modificateur s’applique aussi à l’enregistrement d’une suite entière (testSuite(...).forTypes<...>() ou testSuite(...).forValues(...)). Le moteur génère automatiquement une suite de tests distincte pour chaque type (nommée par exemple "Container/Vector [type 0]", "Container/Vector [type 1]") ou pour chaque valeur (nommée "Math/Prime [2]", "Math/Prime [3]") :

auto _ = testSuite("Container/Vector").forTypes<int, float>() = []<class Type>(TestSuite& suite) {
suite.test("Default initialization") = [] {
auto container = std::vector<Type>{};
expect(std::ranges::empty(container));
};
};
auto _ = testSuite("Math/Prime").forValues(2, 3, 5, 7) = [](TestSuite& suite, int prime) {
suite.test("Is prime") = [prime] {
expect(isPrime(prime));
};
};

Pour tester des architectures événementielles (CppUtils::Execution::EventQueue, CppUtils::Execution::EventDispatcher), CppUtils::UnitTest::EventWatcher s’abonne à un événement à sa construction et propose des outils pour enregistrer l’historique des émissions et bloquer jusqu’à réception du nombre attendu d’occurrences sans boucle manuelle :

suite.test("Event dispatching") = [] {
using namespace std::chrono_literals;
auto queue = CppUtils::Execution::EventQueue{};
// Création d'un observateur d'événements typé sur "Notification"
auto watcher = watch<"Notification", int>(queue);
expect(not watcher.hasOccurred());
expect(watcher.count()) == 0uz;
queue.emit<"Notification">(42);
queue.emit<"Notification">(84);
// Attente active jusqu'à réception de 2 occurrences dans la limite de 500ms
expect(watcher.waitForOccurrences(2uz, 500ms));
expect(watcher.count()) == 2uz;
auto occurrences = watcher.occurrences();
expect(occurrences->count) == 2uz;
expect(std::ranges::size(occurrences->payloads)) == 2uz;
expect(occurrences->payloads[0]) == 42;
expect(occurrences->payloads.back()) == 84;
};

Pour tester la conformité sémantique de vos objets (conteneurs, wrappers RAII, types à sémantique de déplacement, optimisations sans copie), CppUtils fournit CppUtils::UnitTest::LifetimeChecker, un objet inerte conçu pour suivre avec précision le cycle de vie de vos types (constructions, destructions, copies et déplacements) et notifier l’appelant.

Vérification des compteurs de copie et de déplacement

Section intitulée « Vérification des compteurs de copie et de déplacement »

L’objet conserve un compteur interne du nombre de copies et de déplacements subis :

import CppUtils;
suite.test("Move semantics in vector") = [&] {
// Objet inerte avec callback vide
auto checker = CppUtils::UnitTest::LifetimeChecker{[](std::string_view) {}, "Widget"};
auto container = std::vector<CppUtils::UnitTest::LifetimeChecker>{};
container.reserve(1);
// Déplacement dans le conteneur
container.push_back(std::move(checker));
expect(container.front().getCopyCount()) == 0uz;
expect(container.front().getMoveCount()) == 1uz;
};

Le constructeur accepte une fonction de rappel (callback) et un identifiant textuel. À chaque opération sur l’objet (construction, copie, déplacement, destruction), ce rappel est automatiquement invoqué avec le libellé de l’événement correspondant :

suite.test("Lifecycle notifications") = [&] {
auto checker = CppUtils::UnitTest::LifetimeChecker{[](std::string_view event) {
std::println("{}", event);
}, "Resource"};
auto copy = checker;
auto moved = std::move(copy);
};

La sortie console retrace fidèlement la succession d’appels et l’invalidation de l’objet déplacé :

Resource()
Resource(const Resource&)
Resource(Resource&&)
~[invalid object]()
~Resource()
~Resource()

Le lancement de l’ensemble des suites s’effectue dans le point d’entrée principal (main.cpp) de votre binaire de tests via executeTests() :

import std;
import CppUtils;
auto main(int argc, char** argv) -> int
{
// Analyse les arguments en ligne de commande (mode, filter, verbose, failFast, chrono)
auto settings = CppUtils::UnitTest::executeCommands(argc, argv);
if (settings.abort)
return CppUtils::exitSuccess;
return CppUtils::UnitTest::executeTests(settings);
}
OptionTypeValeur par défautDescription
verbosebooltrueAffiche l’intitulé et le statut de chaque test unitaire en temps réel. Si false, n’affiche que le bilan final.
detailbooltrueAffiche les détails d’exécution (séparateurs, noms complets de suites) lors du déroulement.
chronoboolfalseMesure et affiche la durée précise de chaque test via CppUtils::Log::ChronoLogger.
failFastboolfalseInterrompt immédiatement l’exécution des tests dès le premier échec rencontré.
filterstd::string""Filtre les suites de tests à exécuter par sous-chaîne tout en résolvant automatiquement leurs prérequis.
reportstd::string""Exporte les résultats d’exécution dans un fichier de rapport (format junit:chemin.xml ou chemin.xml).

La fonction executeCommands() parse les arguments transmis au binaire (argc, argv) selon les conventions standards POSIX et GNU :

Fenêtre de terminal
# Options standards longues
./UnitTests --filter=Network --chrono --report=junit:report.xml
# Formes courtes équivalentes
./UnitTests -f=Network -c -r=report.xml
# Arrêt immédiat dès la première défaillance (fail-fast)
./UnitTests --fail-fast
Option longueForme courteValeur par défautDescription
--help-h-Affiche l’aide des paramètres disponibles et interrompt le programme.
--mode=<mode>-m=<mode>debugProfil d’exécution prédéfini : debug (défaut), test (avec chronométrage), prod (silencieux et arrêt au premier échec).
--verbose[=<bool>]-v[=<bool>]trueActive ou désactive les journaux standards d’exécution.
--detail[=<bool>]-d[=<bool>]trueActive ou désactive l’affichage détaillé de l’avancement (noms des tests, durées, métriques de benchmarks, séparateurs).
--fail-fastfalseInterrompt l’exécution dès la première erreur.
--chrono[=<bool>]-c[=<bool>]falseAffiche le temps d’exécution de chaque test.
--filter=<chaine>-f=<chaine>""Ne lance que les suites de tests dont le nom contient cette sous-chaîne (et résout automatiquement leurs dépendances).
--report=<chemin>-r=<chemin>""Exporte le rapport d’exécution au format JUnit XML (ex. -r=report.xml ou explicitement --report=junit:report.xml).

Export de rapports JUnit XML et intégration CI/CD

Section intitulée « Export de rapports JUnit XML et intégration CI/CD »

L’option --report=junit:<chemin> (ou simplement -r=<chemin>) génère un rapport au standard universel JUnit XML. Ce format permet aux plates-formes d’intégration continue d’analyser automatiquement les résultats, d’afficher les métriques temporelles et d’insérer des annotations directement sur les lignes de code défaillantes :

  • GitLab CI : Prend en charge le format JUnit XML de manière native (aucun plugin ni composant tiers requis) via artifacts:reports:junit: dans .gitlab-ci.yml.
  • GitHub Actions : Ne parse pas nativement les fichiers XML de tests par défaut. L’affichage d’un résumé graphique et des annotations inline dans les Pull Requests s’effectue via une action GitHub dédiée du marketplace (telle que EnricoMi/publish-unit-test-result-action ou dorny/test-reporter).
  • Jenkins : Exploite directement le rapport via son plugin officiel standard JUnit Plugin.
# .gitlab-ci.yml : Aucun plugin requis, support natif de GitLab CI
test:
stage: test
script:
- xmake run CppUtils-UnitTests --report=junit:build/test-results.xml
artifacts:
when: always
reports:
junit: build/test-results.xml

Isolation par processus et gestion du temps maximal

Section intitulée « Isolation par processus et gestion du temps maximal »

Chaque test unitaire est exécuté dans un processus enfant dédié. Les violations d’accès mémoire, les arrêts brutaux et les blocages infinis sont ainsi interceptés de manière étanche sans jamais interrompre l’exécuteur de tests ni affecter les suites suivantes.

Cette architecture offre deux garanties majeures :

  • Résilience face aux plantages critiques : Si un test déclenche un signal fatal (SIGSEGV, SIGABRT, etc.), le signal est capturé par l’exécuteur parent. Le test est consigné en échec avec le détail du signal reçu, tandis que le reste des tests continue son exécution normale.
  • Interruption stricte en cas de timeout : Si un test dépasse son temps limite alloué (10 secondes par défaut, ou personnalisé via .timeout(2s)), il est interrompu immédiatement, évitant les blocages indéfinis de la suite de tests.

Lors d’une exécution sans erreur, le rapport affiche le déroulement structuré des tests :

Terminal
2 tests found. Execution:
──────────────────────────────────────────────────────────────
Math/Calculator/Addition:
Passed ✓
──────────────────────────────────────────────────────────────
Network/Socket/TCP/Accept connection:
Passed ✓
──────────────────────────────────────────────────────────────
Test results
All tests passed successfully
- 2 successful tests

Défaillance ciblée et propagation aux dépendances

Section intitulée « Défaillance ciblée et propagation aux dépendances »

En cas d’échec d’un composant de base, le rapport isole distinctement la ligne fautive avec les valeurs évaluées, et marque automatiquement les suites dépendantes comme sautées :

Terminal
──────────────────────────────────────────────────────────────
Network/Socket/Init:
Passed ✓
──────────────────────────────────────────────────────────────
Network/Socket/TCP:
                         🛇 ERROR 🛇
The following test didn't pass:
Network/Socket/TCP
  In tests/Network/Socket.mpp
At line 48, column 3
In expect(lhs) == rhs
with lhs:
500
and rhs:
200

Failed ✕
──────────────────────────────────────────────────────────────
Skipping test suite Network/Client due to failed prerequisites in test suite Network/Socket ⚠
──────────────────────────────────────────────────────────────
Test results
The tests failed:
- 1 successful tests
- 1 failed tests
  - Network/Socket
- 1 skipped tests
  - Network/Client (due to failed prerequisites)