fcfTest is a lightweight, header-only unit testing framework for C++. It provides a simple macro, FCF_TEST, which implements all possible checks and displays the current values of variables. The framework also includes test registration, command-line execution, a built-in simple logger, and tools for measuring execution time.
The library is distributed as a single header file: fcfTest/test.hpp.
To use the library, you must define the FCF_TEST_IMPLEMENTATION macro before including the header file in your application's main cpp file to avoid errors due to multiple definitions.
Usage article on dev.to: https://dev.to/vladimirm/fcftest-unit-test-library-3cgg
If something doesn't work for you, please report it quickly in Issues and we'll fix it!
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
#include <vector>
// Declare a test case
FCF_TEST_DECLARE("MyLibraryPartName", "ExamplesGroupName", "VectorSizeTestName"){
std::vector<std::string> vec;
vec.push_back("test");
// Perform an assertion. If false, throws std::runtime_error with details.
FCF_TEST(vec.size() == 2, vec.size());
}
int main(int a_argc, char* a_argv[]){
// Run the test suite via command line interface
bool error;
fcf::NTest::cmdRun(a_argc, (const char**)a_argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}Output: The program will execute the registered tests. If an assertion fails, it prints a descriptive error including file, line number, and variable values. If successful, it prints a summary count of completed tests.
Performing the test: "MyLibraryPartName" -> "ExamplesGroupName" -> "VectorSizeTestName" ...
Comparison started
Test error: vec.size() == 2 [FILE: DIR_PATH/main.cpp:9]
Values:
vec.size(): 1
This macro is used to enable the implementation section within the header file fcfTest/test.hpp.
- Usage: Must be defined (
#define FCF_TEST_IMPLEMENTATION) before including the header if you want to use the library's functionality (e.g., running tests, using the logger). - Purpose: Prevents multiple definition errors by ensuring that global variables and function definitions are only generated once per compilation unit. When omitted, the header provides only declarations.
This macro is used to export symbols from the test library when building it as a DLL or shared library.
- Usage: Defined by the main FCF library macros (
FCF_EXPORT) if applicable, otherwise empty. - Purpose: Ensures consistent symbol visibility for functions and variables defined in
fcfTestwhen compiled as a DLL.
This macro is used to import symbols from the test library when using it as a client application linking against a shared library.
- Usage: Defined by the main FCF library macros (
FCF_IMPORT) if applicable, otherwise empty. - Purpose: Ensures that functions and variables defined in external shared libraries are correctly imported into the client process without multiple definition errors.
The primary macro for performing checks in unit tests.
- Behavior: Evaluates
(a_expression). If the result is false (non-zero), it throws astd::runtime_error. - Error Message: The exception message includes:
- The failing expression (
#exp). - The file name and line number where the macro was called.
- Values of all additional arguments provided in
a_observedVariables.
- The failing expression (
Example:
int x = 5;
FCF_TEST(x == 4, x);
// Throws error: Test error: x == 4 [FILE: main.cpp:10]
// Values:
// x: 5Tests are organized hierarchically into Parts, Groups, and Tests. This allows for filtering execution based on these levels.
Declares a new test case.
- Parameters:
am_part: The name of the logical part (highest level).am_group: The name of the sub-group within the part.am_test: The unique identifier for the test function.
- Usage: This macro generates a static anonymous class that registers the test with the global storage upon instantiation.
Example:
FCF_TEST_DECLARE("Network", "HTTP", "GetRequestTest") {
// Test implementation
}
FCF_TEST_DECLARE("Network", "HTTP", "PostRequestTest") {
// Test implementation
}Registers the execution order for a specific part.
- Parameters:
am_part: Name of the part.am_order: Integer priority (lower values run first).
Example:
FCF_TEST_PART_ORDER("CriticalTests", 1); // Run critical tests first
FCF_TEST_PART_ORDER("GeneralTests", 2);Registers the execution order for a specific group within a part.
- Parameters:
am_group: Name of the group.am_order: Integer priority.
Example:
FCF_TEST_GROUP_ORDER("Integration", 1);
FCF_TEST_GROUP_ORDER("Unit", 2);Registers the execution order for a specific test within a group.
- Parameters:
am_test: Name of the test.am_order: Integer priority.
Example:
FCF_TEST_TEST_ORDER("SmokeTest", 1);
FCF_TEST_TEST_ORDER("LoadTest", 2);The library provides global shortcuts to access streams based on log levels. These are defined in the fcf::NTest namespace and also available globally if included directly or via specific exports.
These functions return references to output streams (std::ostream&) corresponding to specific log levels:
ftl(): Fatal level.err(): Error level.wrn(): Warning level.att(): Attention level.log(): Log level (default).inf(): Information level.dbg(): Debug level.trc(): Trace level.
If the global logger's level is set lower than the requested level, these return a reference to an empty stream buffer (no output).
Example:
fcf::NTest::ftl() << "This will only appear if log level is FTL or higher" << std::endl;
fcf::NTest::dbg() << "This appears only in debug mode" << std::endl;The fcf::NTest::Duration class provides a simple interface for measuring the execution time of code blocks. It uses std::chrono::high_resolution_clock.
- Constructor:
Duration(unsigned long long a_iterations)sets the number of iterations. Default is 1. - Methods:
unsigned long long iterations(): Returns the number of iterations.void begin(): Records start time.void end(): Records end time.std::chrono::nanoseconds totalDuration(): Returns elapsed time for all iterations.std::chrono::nanoseconds duration(): Returns average time per iteration.
- Operator Call:
operator()(TFunctor&& a_functor)executes the functor_iterationstimes, timing the block automatically viabegin()andend().
Example:
// Measure sorting 1000 times
fcf::NTest::Duration bench(1000);
bench([](){
std::vector<int> v = {5, 2, 9};
std::sort(v.begin(), v.end());
});
std::cout << "Total: " << bench.totalDuration().count() << " ns\n";
std::cout << "Avg: " << bench.duration().count() << " ns\n";The test runner supports filtering tests by Part, Group, or specific Test name, and setting the global log level. It uses a command-line argument parser implemented in cmdRun.
This structure holds the configuration for running tests. It is populated automatically by the command line parser but can be used manually.
struct Options {
std::vector<std::string> parts; ///< List of part names to run (empty = all).
std::vector<std::string> groups; ///< List of group names to run (empty = all).
std::vector<std::string> tests; ///< List of specific test names to run (empty = all).
std::string logLevel; ///< Desired logging level ("off", "err", "log", etc.).
};The central function for executing the test suite. It parses command-line arguments and determines the action.
This enum dictates how cmdRun behaves when parsing arguments:
CRM_PARSE: Parses arguments but does not execute tests or show help/list. Returns the determined mode (e.g.,CM_HELP,CM_LIST) so the caller can decide what to do next. Useful for building a custom menu system.CRM_EXECUTE: Parses arguments. If the flag--test-runwas provided, it executes tests. If--test-helpor--test-listwere provided, it displays that information immediately and returns.CRM_RUN: Parses arguments and automatically executes tests unless--test-helpor--test-listis explicitly requested. This is the standard mode for running tests directly from the command line (equivalent:./my_tests --test-run).
int main(int a_argc, char* a_argv[]) {
// Standard execution: Parse and run.
bool error = false;
fcf::NTest::cmdRun(a_argc, (const char**)a_argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}int main(int a_argc, char* a_argv[]) {
// Standard execution: Parse and run by request.
bool error = false;
int mode = fcf::NTest::cmdRun(a_argc, (const char**)a_argv, fcf::NTest::CRM_EXECUTE, &error);
if (error) {
// An error occurred while running the test.
return 1;
}
if (mode != fcf::NTest::CM_NONE){
// The --test-run | --test-help | ---test-list flags were passed at startup.
// And the function performed all the actions
return 0;
}
.... your application code ...
return 0;
}int main(int a_argc, char* a_argv[]) {
// Or custom menu mode: Just parse arguments to see what was asked
fcf::NTest::Options options;
int mode = fcf::NTest::cmdRun(options, a_argc, (const char**)a_argv, fcf::NTest::CRM_PARSE);
if (mode == fcf::NTest::CM_HELP) {
fcf::NTest::cmdHelp();
return 0;
} else if (mode == fcf::NTest::CM_LIST) {
fcf::NTest::cmdList();
return 0;
} else if (mode == fcf::NTest::CM_RUN) {
bool error = false;
fcf::NTest::run(options, &error);
return error ? 1 : 0;
}
// ... default execution
}--test-run: Forces execution of selected tests.--test-list: Prints a list of all available tests in the hierarchy (Part -> Group -> Test).--test-help: Displays usage information and available flags.--test-log-level LEVEL: Sets the global logging verbosity (e.g.,dbg,log,err).--test-part PART_NAME: Filters execution to only tests belonging to the specified part. Can be used multiple times.--test-group GROUP_NAME: Filters execution to only tests belonging to the specified group. Can be used multiple times.--test-test TEST_NAME: Filters execution to run only the specific test named. Can be used multiple times.
Example Command:
./my_tests --test-run --test-part Network --test-group HTTP --test-log-level dbgcmdHelp(): Displays help information and available command-line flags.cmdList(): Displays a list of all registered tests with their hierarchy structure.run(const Options& a_options, bool* a_errorPtr = 0): Executes the selected tests based on anOptionsobject. Usually called internally bycmdRun. If a null pointer is passed, the function throws an exception when an error occurs.
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
#include <vector>
#include <cmath>
// --- Test Declarations ---
FCF_TEST_DECLARE("Math", "BasicArithmetic", "Addition") {
int a = 2;
int b = 3;
FCF_TEST(a + b == 5, a, b);
}
FCF_TEST_DECLARE("Math", "BasicArithmetic", "Subtraction") {
int a = 10;
int b = 4;
FCF_TEST(a - b == 6, a, b);
}
FCF_TEST_DECLARE("Vector", "SizeCheck", "EmptyVector") {
std::vector<int> v;
FCF_TEST(v.size() == 0, v.size());
}
// --- Order Registration ---
// Run Math tests before Vector tests
FCF_TEST_PART_ORDER("Math", 1);
FCF_TEST_PART_ORDER("Vector", 2);
// Run "BasicArithmetic" group first within Math part
FCF_TEST_GROUP_ORDER("BasicArithmetic", 1);
// Run Addition test first
FCF_TEST_TEST_ORDER("Addition", 1);
int main(int a_argc, char* a_argv[]) {
// Use CRM_RUN for standard execution
bool error;
fcf::NTest::cmdRun(a_argc, (const char**)a_argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}