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

Mastering Test Fixtures and Data Sharing

In professional software testing, tests rarely exist in isolation. They often require a specific environment: a database connection, a pre-populated cache, a file system state, or a specific configuration. Manually repeating this setup in every test violates the DRY (Don't Repeat Yourself) principle and makes your test suite difficult to maintain.

The fcfTest library solves this problem using Fixtures. Fixtures allow you to define common setup and teardown logic that is automatically executed at different levels of your test hierarchy.

Fixture Lifecycle Levels

One of the most powerful features of fcfTest is the ability to control when the fixture code runs. This is determined by the am_level argument in the FCF_TEST_BEFORE_DEFINE macro.

Level Behavior
fcf::NTest::FL_GLOBAL Global scope. The setup runs once before any tests in the entire suite start, and the teardown runs once after all tests are finished.
fcf::NTest::FL_PART Partition scope. The setup runs once before the first test in a specific Part begins, and the teardown runs after the last test in that Part completes.
fcf::NTest::FL_GROUP Group scope. The setup runs once before the first test in a Group starts, and the teardown runs after the last test in that Group finishes.
fcf::NTest::FL_TEST Individual test scope. The setup runs immediately before a specific test, and the teardown runs immediately after it.
Targeting Tests with Selectors

Fixtures are not applied to everything by default. You must specify which part of the hierarchy they belong to using selectors (Part, Group, Test). You can use the wildcard symbol "*" to apply a fixture to all elements at a certain level.

  • "MyPart", "*", "*" — Applies to everything within "MyPart".
  • "*", "MyGroup", "*" — Applies to all tests within "MyGroup", regardless of which Part they belong to.
  • "*", "*", "TestName" — Applies only to one specific test.
Data Sharing: The State Mechanism

The most common use case for fixtures is passing data to the tests (e.g., a pointer to a database object). Since fixtures and tests are separate code blocks, fcfTest provides a thread-safe global singleton, state, to facilitate this.

To share data, follow these three steps:

  1. In the Fixture (Setup): Store data using data and wrap it in a fcf::NTest::SharedPtrAny using make.
    state().data("key", SharedPtrAny::make<int>(42));
  2. In the Test: Retrieve the data using the same key and cast it back to its original type.
    int* val = state().data("key").cast<int>();
  3. In the Fixture (Teardown): It is good practice to clean up the data using eraseData to prevent side effects in other tests.
    state().eraseData("key");
Comprehensive Example

The following example demonstrates a complete application where a global fixture sets an app version, a group fixture manages a simulated database connection, and a test retrieves both pieces of data.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> #include <string> #include <iostream> // --- 1. GLOBAL FIXTURE --- // This runs once at the very start of the entire test execution. FCF_TEST_BEFORE_DEFINE("System", "*", "*", fcf::NTest::FL_GLOBAL) { fcf::NTest::log() << "[GLOBAL] Initializing system environment..." << std::endl; // Store a global configuration string fcf::NTest::state().data("app_version", fcf::NTest::SharedPtrAny::make<std::string>("1.2.5")); } FCF_TEST_AFTER_DEFINE("System", "*", "*", fcf::NTest::FL_GLOBAL) { fcf::NTest::log() << "[GLOBAL] System shutdown complete." << std::endl; } // --- 2. GROUP FIXTURE --- // This runs once for every test inside the "Database" group. FCF_TEST_BEFORE_DEFINE("System", "Database", "*", fcf::NTest::FL_GROUP) { fcf::NTest::log() << "[GROUP] Opening database connection..." << std::endl; // Simulate a connection string being created fcf::NTest::state().data("db_conn", fcf::NTest::SharedPtrAny::make<std::string>("SQL_CONNECTION_ACTIVE")); } FCF_TEST_AFTER_DEFINE("System", "Database", "*", fcf::NTest::FL_GROUP) { fcf::NTest::log() << "[GROUP] Closing database connection..." << std::endl; // Clean up the group-specific data fcf::NTest::state().eraseData("db_conn"); } // --- 3. TEST DEFINITIONS --- FCF_TEST_DEFINE("System", "Database", "ConnectionTest") { // Retrieve data from Global fixture std::string* version = fcf::NTest::state().data("app_version").cast<std::string>(); // Retrieve data from Group fixture std::string* conn = fcf::NTest::state().data("db_conn").cast<std::string>(); fcf::NTest::log() << "Running test with App Version: " << *version << std::endl; FCF_TEST(version != nullptr); FCF_TEST(conn != nullptr); FCF_TEST(*conn == "SQL_CONNECTION_ACTIVE"); } FCF_TEST_DEFINE("System", "Database", "QueryTest") { // Verify that the connection is still valid for the second test in the group std::string* conn = fcf::NTest::state().data("db_conn").cast<std::string>(); FCF_TEST(conn != nullptr); FCF_TEST(*conn == "SQL_CONNECTION_ACTIVE"); } int main(int a_argc, char* a_argv[]) { bool error = false; fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }
Expected Output:
  > [GLOBAL] Initializing system environment... > [GROUP] Opening database connection... Performing the test: "System" -> "Database" -> "ConnectionTest" ... > Running test with App Version: 1.2.5 [SUCCESS] Test completed successfully (0.000`007`376 sec) Performing the test: "System" -> "Database" -> "QueryTest" ... [SUCCESS] Test completed successfully (0.000`001`064 sec) > [GROUP] Closing database connection... > [GLOBAL] System shutdown complete. [SUCCESS] All tests were completed. Tests: 2 passed, 0 failed, 0 skipped, 2 total Duration: 0.000`008`440 sec