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

Parameterized Testing: Running the Same Logic with Different Data

In many testing scenarios, you need to verify the same logic against multiple sets of data. For example, testing a mathematical function with various integers, or checking a parser with different string inputs. Instead of duplicating your test code for every single case, fcfTest allows you to use Parameterized Tests.

A parameterized test is a single test definition that the framework executes multiple times, once for each parameter provided in a set. This is also known as Data-Driven Testing.

Accessing Parameters in a Test

To use a parameter inside your test, you must retrieve it from the global test state. The param method returns a fcf::NTest::SharedPtrAny object containing the current parameter value.

Since the parameter is stored in a type-agnostic container, you must cast it to the expected type before use.

FCF_TEST_DEFINE("Math", "Arithmetic", "SquareTest") { // 1. Retrieve the current parameter fcf::NTest::SharedPtrAny p = fcf::NTest::state().param(); // 2. Cast it to the desired type int* val = p.cast<int>(); // 3. Use it in your assertions FCF_TEST(*val * *val >= 0); }

Ways to Provide Parameters

There are three primary ways to inject parameters into your tests: by configuring the storage before the test runner starts, by using fixtures, or by injecting them dynamically during the first iteration of a test.

Method 1: Manual Injection via Storage

This method is ideal when you want to define parameters globally or within your main function. You use the appendParamValue method to register a sequence of values for a specific test path.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> // A test that expects an integer parameter FCF_TEST_DEFINE("Math", "Arithmetic", "SquareTest") { // Retrieve the parameter from the state fcf::NTest::SharedPtrAny p = fcf::NTest::state().param(); int* val = p.cast<int>(); fcf::NTest::log() << "Testing square of: " << *val << std::endl; FCF_TEST(*val * *val >= 0); } int main(int a_argc, char* a_argv[]) { bool error = false; // Inject parameters for the specific test before calling the runner. // This tells fcfTest: "Run SquareTest 3 times with these specific values." fcf::NTest::storage().appendParamValue("Math", "Arithmetic", "SquareTest", 2, 5, 10); // Start the test execution fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }
Output:
Performing the test: "Math" -> "Arithmetic" -> "SquareTest" ... == Parameter set: 1 > Testing square of: 2 Parameter status: success (duration: 0.000`010`951 sec) == Parameter set: 2 > Testing square of: 5 Parameter status: success (duration: 0.000`006`675 sec) == Parameter set: 3 > Testing square of: 10 Parameter status: success (duration: 0.000`006`461 sec) [SUCCESS] Test completed successfully (0.000`024`087 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`024`087 sec

Method 2: Contextual Injection via Fixtures

If you want a group of tests to share a set of parameters, use a Fixture. By using the FCF_TEST_BEFORE_DEFINE macro with a specific lifecycle level (like FL_GROUP), you can inject parameters automatically whenever that group is entered.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> #include <string> // 1. Define a fixture for the "Settings" group. // This runs once before the first test in the "Settings" group starts. FCF_TEST_BEFORE_DEFINE("Config", "Settings", "*", fcf::NTest::FL_GROUP) { fcf::NTest::log() << "Injecting mode parameters for the group..." << std::endl; // Inject parameters for the 'ModeTest' located in this group fcf::NTest::storage().appendParamValue("Config", "Settings", "ModeTest", std::string("Fast"), std::string("Secure")); } // 2. The test definition that uses the parameters FCF_TEST_DEFINE("Config", "Settings", "ModeTest") { // Retrieve the parameter fcf::NTest::SharedPtrAny p = fcf::NTest::state().param(); std::string* mode = p.cast<std::string>(); fcf::NTest::log() << "Current mode: " << *mode << std::endl; FCF_TEST(!mode->empty()); } 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; }
Output:
  > Injecting mode parameters for the group... Performing the test: "Config" -> "Settings" -> "ModeTest" ... == Parameter set: 1 > Current mode: Fast Parameter status: success (duration: 0.000`006`327 sec) == Parameter set: 2 > Current mode: Secure Parameter status: success (duration: 0.000`004`907 sec) [SUCCESS] Test completed successfully (0.000`011`234 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`011`234 sec

Method 3: Dynamic Injection during Test Execution

In some advanced cases, you might want to decide which parameters to run with based on logic discovered during the test execution. You can use the paramIndex method to detect the very first iteration and then inject new parameters into the storage dynamically.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> FCF_TEST_DEFINE("ParamDemo", "Index", "IndexTest") { // 1. On the first call, you can add parameters for the test // The value of fcf::NTest::State::param() will also be updated! if (fcf::NTest::state().paramIndex() == 0) { fcf::NTest::storage().appendParamValue(fcf::NTest::state().test(), 10); fcf::NTest::storage().appendParamValue(fcf::NTest::state().test(), 20); } // 2. Get the current index size_t currentIndex = fcf::NTest::state().paramIndex(); fcf::NTest::log() << "Currently processing parameter index: " << currentIndex << std::endl; // 3. Get the current parameter fcf::NTest::SharedPtrAny currentParam = fcf::NTest::state().param(); fcf::NTest::log() << "Currently processing parameter value: " << *currentParam.cast<int>() << std::endl; } 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; }
Output:
Performing the test: "ParamDemo" -> "Index" -> "IndexTest" ... == Parameter set: 1 > Currently processing parameter index: 0 > Currently processing parameter value: 10 Parameter status: success (duration: 0.000`032`038 sec) == Parameter set: 2 > Currently processing parameter index: 1 > Currently processing parameter value: 20 Parameter status: success (duration: 0.000`009`506 sec) [SUCCESS] Test completed successfully (0.000`041`544 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`041`544 sec

Summary of Approaches
Approach Best Used For... Mechanism
Manual Injection One-off tests or specific scenarios defined in your main execution logic. storage().appendParamValue(...) in main.
Fixture Injection Applying the same data sets to an entire group, part, or the whole suite. appendParamValue inside BEFORE_DEFINE.
Dynamic Injection Adding parameters on-the-fly based on logic or state discovered during the first run. appendParamValue inside FCF_TEST_DEFINE using paramIndex().