FCF 2.0 development in progress...
> > >
[News] [C++ Libraries API] [C++ Downloads] [Donate to the project] [Contacts]

About the fcfTest library

Here are the main features of the fcfTest unit testing library.
1 macros in all cases.

The library's main feature is the FCF_TEST macro. It doesn't just test a logical expression; it works as an intelligent debugger. When a test fails, the library automatically extracts variable names and their current values.

Example: Comparison with the traditional approach

Traditional approach (without fcfTest)

int expected = 100; int actual = calculate_value(); if (actual != expected) { std::cout << "Error: expected " << expected << " but got " << actual << std::endl; throw std::runtime_error("Test failed"); }

Approach with fcfTest

int expected = 100; int actual = calculate_value(); FCF_TEST(actual == expected, expected, actual);

If actual is 95, you will get an instant and clear report:

Test error: actual == expected [FILE: main.cpp:12] Values: expected: 100 actual: 95

Zero-Configuration: One File and You're Up and Running

fcfTest is a header-only library. You don't need to configure CMake, download binaries, or mess with linking. This makes it an ideal choice for microservices, small utilities, or embedded systems.

Just add the file to the project:

// In your main.cpp file #define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> FCF_TEST_DEFINE("Core", "Math", "AdditionTest") { FCF_TEST(2 + 2 == 4); } int main(int a_argc, char* a_argv[]) { bool error; // We run all tests declared in the application fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }
Output:
Performing the test: "Core" -> "Math" -> "AdditionTest" ... [SUCCESS] Test completed successfully (0.000`000`276 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`000`276 sec

Hierarchy and Order: Complete Control

In large projects, tests often depend on system state. fcfTest offers a three-level structure: Part -> Group -> Test. You can run only the required sections or specify a strict execution order.

Example: Controlling Execution Order
// Declare tests FCF_TEST_DEFINE("Database", "Connection", "InitTest") { /* ... */ } FCF_TEST_DEFINE("Database", "Query", "SelectTest") { /* ... */ } // Ensure that initialization occurs first, then queries FCF_TEST_PART_ORDER("Database", 1); FCF_TEST_GROUP_ORDER("Connection", 1); FCF_TEST_GROUP_ORDER("Query", 2);

Running via the command line allows you to filter tests without recompiling:

// Run only tests from the "Query" group ./my_tests --test-group Query // Run everything except the "Legacy" group ./my_tests --test-ignore-group Legacy

Fixture Support

Fixtures can be attached to any level of the test hierarchy (Part -> Group -> Test). You have full control over when they are executed, allowing common setup and cleanup logic to be shared efficiently.

FCF_TEST_BEFORE_DEFINE("Math", "*", "*", fcf::NTest::FL_GROUP) { // Initialize shared data when a new group starts fcf::NTest::state().data("fixture_data", fcf::NTest::SharedPtrAny::make<std::string>("MyData")); } //This fixture will be held after the "Mathematics: Basics" group. FCF_TEST_AFTER_DEFINE("Math", "*", "*", fcf::NTest::FL_GROUP) { // Clean up the shared state when leaving the fixture scope fcf::NTest::state().eraseData("fixture_data"); }

This fixture is executed whenever a new group within the Math part begins, and its cleanup counterpart is executed when that group completes.


Test Parameter Support

You can run a single test with different parameters by specifying them using the API.

Example of a parameterized test with command-line parameter input:

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> // In the fixture we set parameters that will always be added FCF_TEST_BEFORE_DEFINE("*", "*", "*", fcf::NTest::FL_GLOBAL){ fcf::NTest::TestPath path = {"Library", "Math", "sum"}; fcf::NTest::storage().appendParamValue(path, std::string("one") , std::string("two") , std::string("three")); } FCF_TEST_DEFINE("Library", "Math", "sum"){ // Get the test parameter value fcf::NTest::log() << "Current test parameter: " << *fcf::NTest::state().param().cast<std::string>() << std::endl; } int main(int a_argc, char* a_argv[]) { // Add test parameters passed via the command line for(int argIndex = 1; argIndex < a_argc; ++argIndex) { if (std::strcmp(a_argv[argIndex], "--param") == 0 && (argIndex + 1) < a_argc) { fcf::NTest::storage().appendParamValue("Library", "Math", "sum", std::string(a_argv[argIndex+1])); ++argIndex; } } // Start testing execution bool error = false; fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }

Example of application launch:

$ app --param "User-entered parameter"

Application output:

Performing the test: "Library" -> "Math" -> "sum" ... == Parameter set: 1 > Current test parameter: User-entered parameter == Parameter set: 2 > Current test parameter: one == Parameter set: 3 > Current test parameter: two == Parameter set: 4 > Current test parameter: three [SUCCESS] Test completed successfully (0.000`022`794 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`022`794 sec

JUnit XML format

The fcfTest framework supports exporting results to JUnit XML format for CI/CD integration using command-line arguments.

For example, to write a test report in JUnit format to a separate file, simply use the --test-file-junit parameter:

$ test --test-file-junit=report.xml

If you want to change the terminal output format, you can use the --test-format command-line parameter.

$ test --test-format junit

<?xml version="1.0" encoding="UTF-8"?> <testsuites tests="1" failure="0" skipped="0" time="0.000028178"> <testsuite name="Performance/Sort" tests="1" failure="0" skipped="0" time="0.000028178"> <testcase classname="Performance/Sort" name="VectorSort" time="0.000028178"/> </testsuite> </testsuites>

The generated XML reports can be used in GitHub Actions to display test results in the workflow summary.

Example: GitHub Actions Integration Example (.github/workflows/ci.yml):

name: C++ CI with fcfTest # Trigger the workflow on push or pull request events for the main branch on: push: branches: [ "main" ] pull_request: branches: [ "main" ] workflow_dispatch: # Allows manual trigger from the GitHub Actions tab jobs: build_and_test: name: Build and Run Tests runs-on: ubuntu-latest # Required permissions to write test check runs and annotate commits/PRs permissions: contents: read checks: write pull-requests: write steps: # Step 1: Check out the repository code - name: Checkout Code uses: actions/checkout@v4 # Step 2: Configure and compile the C++ project using CMake - name: Configure and Build run: | cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release # Step 3: Run the test executable and export results to a JUnit XML file # The --test-file-junit flag tells fcfTest where to save the XML report - name: Run fcfTest with JUnit Output run: | ./build/my_test_executable --test-file-junit=report.xml # Step 4: Visualize the JUnit XML report directly in the GitHub UI # Uses the 'always()' condition to ensure the report is published even if tests fail - name: Publish Test Report uses: mikepenz/action-junit-report@v4 if: always() with: report_paths: '**/report.xml' detailed_summary: true include_passed: true

More than just tests: Logger and Benchmarking

Why bother with three different libraries when you can use one? fcfTest is a developer's all-in-one solution.

Built-in Logger

You can log test execution with different severity levels. This is useful for monitoring system state during long tests.

// Perform default logging fcf::NTest::log() << "Starting heavy data processing..." << std::endl; // First, save the current configuration fcf::NTest::Logger::Prefixes prefixes = fcf::NTest::logger().prefixes(); // Remove the current prefixes fcf::NTest::logger().clearPrefixes(); // You can add a custom prefix (e.g., time) fcf::NTest::Logger::Prefix prefix; prefix.name = "my-prefix"; prefix.prefix = "[LOG]: "; prefix.category = fcf::NTest::LMC_USER_GROUP; prefix.multiLine = false; logger.appendPrefix(prefix); // Perform logging with the new settings fcf::NTest::log() << "Some actions have been completed" << std::endl; // Restore the previous settings fcf::NTest::logger().prefixes(prefixes);
Output:
> Starting heavy data processing... > [LOG]: Some actions have been completed

Custom Output Formatting

If the built-in output formats do not meet your requirements, you can easily define and register your own custom formatting handler.

// Create a new format fcf::NTest::Logger::Format customFormat; customFormat.name = "my-custom-format"; customFormat.handler = [](fcf::NTest::Logger&, fcf::NTest::Logger::MessageContext& a_context) { a_context.message = "[message: " + (std::stringstream() << std::hex << a_context.category).str() + "]\n"; }; // Add the format to the logger logger.appendFormat(customFormat);

Output:

[message: 20002] [message: 40001] [message: 10008] [message: 10003] [message: 10005] [message: 10006]

Built-in Benchmarking

The Duration class lets you measure code performance directly within tests. This turns a regular unit test into a performance verification tool.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> FCF_TEST_DEFINE("Performance", "Sort", "VectorSort") { fcf::NTest::Duration bench(1000); // 1000 iterations std::vector<int> sorted; bench([&sorted]() { sorted = {5, 2, 9, 1}; std::sort(sorted.begin(), sorted.end()); }); FCF_TEST(std::is_sorted(sorted.begin(), sorted.end())); fcf::NTest::inf() << "Avg time: " << bench.duration().count() << " ns" << std::endl; } int main(int a_argc, char* a_argv[]) { bool error; // We run all tests declared in the application fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }
Run:
$ test --test-log-level inf
Output:
Performing the test: "Performance" -> "Sort" -> "VectorSort" ... > Avg time: 21 ns [SUCCESS] Test completed successfully (0.000`030`622 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`030`622 sec