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(). |