The project's testing doctrine: frameworks, conventions, and the invariants every change must protect. This page is the single source of truth for testing rules.
- C++ tests use GoogleTest. Only GoogleTest. This covers the kernel and the editor. Catch2, doctest, and hand-rolled frameworks are forbidden by project decision — see ADR 0004. Do not reintroduce Catch2 even though early project material mentioned it.
- Python tests use pytest, leaning into pytest idioms — no unittest-style classes.
- googletest is pinned (URL + SHA256, BSD-3-Clause) in
cmake/deps.cmake; test targets build only underRM_BUILD_TESTS(on by default and in every preset).
- Write tests with the code, not after. A PR that adds behavior without tests is incomplete — see the PR checklist.
- Test locations:
core/tests/(kernel),editor/tests/(editor, headless),python/tests/(bindings),tests/consume_installed/(installed-package smoke, exercised by CI). - Run everything with
ctest --preset dev-<os>andpytest python/tests(see Building). Add-j Nfor a ~5× faster wall clock — CI runs-j 4, so parallel-safety is part of the test contract: every test case runs as its own process, and any state a test shares with a sibling through the OS (QSettings domains, fixed file paths, ports) must be scoped per test. The QSettings idiom is a per-test application name derived fromcurrent_test_info()(seeeditor/tests/test_welcome_widget.cpp); useQTemporaryDirfor files.
- One
TEST(SuiteName, TestName)per behavior. Suites are PascalCase nouns matching the unit under test (Arena,XodrReader,RoundTrip); test names are PascalCase sentences (SlotReuseBumpsGeneration). ASSERT_*when continuing makes no sense — null checks, container sizes before indexing,Expected::has_value()before dereferencing.EXPECT_*everywhere else, so a single run reports every divergence.- Geometry comparisons use
EXPECT_NEAR(value, expected, tol)with named tolerances fromrm::tol. NeverEXPECT_DOUBLE_EQfor computed geometry; never magic epsilons. - Compile-time facts are checked with
static_assertinside the test body (GoogleTest has no STATIC_REQUIRE). - Test helpers that return a value throw on setup failure (GoogleTest
reports uncaught exceptions as test failures); void helpers may use
ASSERT_*/EXPECT_*directly. - Use
SCOPED_TRACEinside sampling loops so a failure names the station (s-coordinate) that diverged. - CMake wiring: link
GTest::gtest_mainand register withgtest_discover_tests(the rootCMakeLists.txtalready doesinclude(GoogleTest)). - gmock is off — prefer small hand-written fakes over mocks.
Editor logic lives in testable model/document classes and is tested with GoogleTest like everything else, headless:
- Tests run with
QT_QPA_PLATFORM=offscreen, set in two places:editor/tests/qt_gtest_main.cpp(so the binary works standalone) and as a ctestENVIRONMENTproperty (soctestworks regardless of shell). - The offscreen platform provides no real OpenGL 3.3 context — never
unit-test
paintGL/the GL renderer; the packaged-binary smoke tests in the release workflow cover that path. Qt6::Testis linked only for helper classes —QSignalSpyandQAbstractItemModelTester. It is never the test runner; GoogleTest is.- Every new
QAbstractItemModelships itsQAbstractItemModelTesterGoogleTest in the same commit — the tester catches most model-index and signal-contract bugs for free.
- Plain test functions plus fixtures; no
unittest.TestCase. tmp_pathfor any file I/O;pytest.raisesfor error contracts;pytest.approx(..., abs=...)for float comparisons, mirroring therm::tolvalues used on the C++ side;@pytest.mark.parametrizewhen one behavior spans many inputs.- Setup:
pip install -e python/
pytest python/tests- Round-trip invariants are first-class tests. Author → write → parse →
compare within
rm::tol::kRoundTrip*tolerances. Any change to the OpenDRIVE writer or reader must keep them green. - Geometry gets golden analytic cases plus property-style checks — known closed-form results on one hand; invariants like arc-length monotonicity and G1 continuity at joints sampled along the curve on the other.
- Parser changes extend the fuzz corpus. Every new OpenDRIVE feature adds
representative inputs to
core/tests/fuzz/corpus/; CI runs a fuzz smoke over it (see CI). Build the fuzzers locally with-DRM_BUILD_FUZZERS=ON(Clang only).
Run an ASan+UBSan build before merging anything touching geometry or parsing:
cmake -B build-asan -G Ninja -DCMAKE_CXX_COMPILER=clang++ \
-DRM_BUILD_TESTS=ON -DRM_SANITIZE=address,undefined
cmake --build build-asan && ctest --test-dir build-asan --output-on-failureCI runs the same configuration on Linux/Clang (with the editor enabled — ASan
surfaces signal/slot lifetime bugs) as a required job, in parallel
(ctest -j 4) plus a seeded random-op soak — see CI for the job's
policy details and the
2026-07 test-suite audit for the measurements
behind them. On macOS, drop detect_leaks (LeakSanitizer is Linux-only).