Understanding the Problem: Catch2 Tests Not Running
Background
Catch2 discovers and registers test cases using static initialization. Unlike frameworks with manual registration APIs, this can break silently if compiler optimizations, linker settings, or build strategies interfere with symbol retention. In CI environments or plugin-based architectures, this leads to missing test cases despite no apparent compilation errors.
Architectural Implications
In enterprise-scale systems, unit tests may be defined across multiple dynamic/shared libraries. Each module may include its own Catch2-based tests. If test cases are compiled but not linked into the final test runner binary correctly, they won't execute. Additionally, compiler flags like LTO (Link Time Optimization), `--gc-sections`, or aggressive inlining may discard static initializers needed by Catch2.
Root Causes
- Tests compiled in separate shared objects or DLLs not linked into the main test binary.
- Use of `--gc-sections` or `-ffunction-sections` causing the linker to drop unused symbols, including test registrations.
- Static initializers eliminated due to LTO or aggressive compiler optimization.
- Test binaries built with `main()` in separate translation units missing linkage to test cases.
- Duplicate Catch2 definitions across modules causing ODR (One Definition Rule) violations.
Diagnosing the Missing Test Case Issue
Step 1: Enable Verbose Output
Use Catch2's `-v` flag during execution to ensure discovery is attempted:
./my-tests -v
Step 2: Compile With `-Wl,--whole-archive`
This linker flag forces inclusion of all object files in a static archive to preserve test registration logic.
g++ -Wl,--whole-archive -lmytestlib -Wl,--no-whole-archive -o my-tests
Step 3: Verify Initialization Symbols
Use tools like `nm` or `objdump` to inspect whether test registration symbols are retained:
nm libtests.a | grep -i catch
Step 4: Consolidate Entry Point
Ensure your `main()` function and test cases are compiled together or explicitly linked into the test binary.
Best Practices and Long-Term Fixes
1. Use a Centralized Test Runner
Aggregate all test cases into one executable by linking all test modules statically. Avoid runtime-loaded test libraries unless using explicit dynamic registration.
2. Disable Symbol Stripping for Test Builds
Modify build configs to exclude `-s`, `--gc-sections`, or `-Wl,-dead_strip` for test binaries to preserve registration logic.
3. Use Static Registration Enforcement
Explicitly force references to test-containing object files in the runner to avoid linker exclusion.
extern int my_test_dummy; volatile int* force_link = &my_test_dummy;
4. Disable LTO for Test Targets
Link Time Optimization can discard static initializers. Add `-fno-lto` or remove LTO flags from test-specific build targets.
5. Validate With CI Symbol Checks
Integrate static analysis in CI pipelines to verify that test registration symbols exist in final binaries.
Conclusion
Catch2's elegance relies heavily on C++ static initialization, which makes it vulnerable to modern linker optimizations and modular build architectures. To ensure test discoverability and reliability at scale, teams must design test runners that explicitly preserve registration logic, unify compilation and linkage strategies, and configure build pipelines to treat test targets as first-class artifacts. Consistency in build environments and linking behavior is key to eliminating elusive missing-test scenarios in Catch2.
FAQs
1. Why are some Catch2 test cases compiled but not executed?
They may be compiled but dropped during linking due to static initializers being optimized away or unused object files not being pulled in.
2. How can I force inclusion of test modules?
Use linker flags like `--whole-archive` or declare dummy references to test module symbols from your test runner.
3. Can dynamic libraries work with Catch2?
Yes, but only if those libraries are explicitly loaded or linked at runtime and contain references that preserve test registrations.
4. Is it safe to use LTO with Catch2?
LTO can strip out static initializers. For test builds, it's recommended to disable LTO unless all initialization behavior is verified.
5. How do I check which tests are registered?
Run the binary with `--list-tests` to output all registered test cases. If your tests don't appear, the issue lies in registration or linkage.