Testing
This page collects how tests are written for LibXR (C++) and for XRobot, LibXR_CppCodeGenerator and xr-syntax (Python). Test code follows the same Code Style and Python Code Style as the main code; this page covers only what is specific to tests. The tests and checks each repository runs in CI, and the commands to run locally before submitting, are listed in How to Contribute.
What to check
- Tests check observable results: return values, command output, error messages, generated files, compilation and run results. They do not check intermediate results of private functions or the order of calls, except when the number of calls is itself the requirement, for example that the xr-syntax builder parses only once, in
build(). - One test checks one behavior, and its name states that behavior. A command-line test may run several commands of one usage scenario to check that each command reaches its implementation; the details of each command are checked in the tests of the module that implements it.
- Every test must be needed: removing it lets some mistake go unnoticed by all other tests. Tests that only change the input, run the same code and assert the same result are merged into one. See Ablation for how to check this.
- A test added with a bug fix must fail on the code before the fix.
- Error messages and short output are compared in full. Temporary paths and commits inside a message are built by the test with f-strings; the output of external programs such as git changes between versions, so only the relevant part of it is compared. Long output such as generated headers or JSON is compared by its relevant parts, or byte for byte against a reference file.
Rejected (checks one word of the message; the rest of the message could be wrong):
with self.assertRaisesRegex(ValueError, "ambiguous"):
constructor_for(model, [{"value": "Read()"}], {}, "Foo", {})
Accepted:
with self.assertRaisesMessage(
ValueError, "Foo: constructor is ambiguous for the supplied names and explicit types"
):
constructor_for(model, [{"value": "Read()"}], {}, "Foo", {})
assertRaisesMessage comes from the test base class fixtures.TestCase and compares the error text exactly; command-line tests compare stderr with fails(..., message=...).
Where tests go
Python repositories:
tests/test_<module>.pybelongs tosrc/<package>/<module>.py. Argument parsing, output format and exit codes of the command line go totest_cli.py; behavior that a command triggers but another module implements goes to that module's test file.- Helpers shared by several test files live in
tests/fixtures.py(conftest.pyin pytest repositories); a helper used by one file stays in that file. - In the
unittestrepositories (XRobot, LibXR_CppCodeGenerator), test classes group behavior and are named with noun phrases (AddInstance,LockFile), deriving fromfixtures.TestCaseor one of its subclasses. Test method names start withtest_and the rest is a sentence stating the behavior (test_a_missing_lock_is_reported). The tests of xr-syntax are module-level functions whose names follow the same rule.
LibXR:
test/automatic/follows the interface under test: the source path without its extension is the test directory, for examplesrc/core/libxr_pipe.hppbelongs toautomatic/core/libxr_pipe/. Tests that need a particular system or peripheral go totest/manual/.- Results are checked with
TEST_ASSERT; a new test lists its source inautomatic/CMakeLists.txtand is called frommain.cppor an existing entry. See the repository's test/README.md.
Comments
- A Python test file starts with a docstring of one Chinese and one English line stating what the file tests; each test class has a one-line bilingual docstring too. Test methods are described by their names and have no docstring. Helper functions, test base classes and fixtures have bilingual docstrings like the main code, and the docstring check of each repository covers
tests/as well (tests/test_docstrings.pyin XRobot and LibXR_CppCodeGenerator,tools/check_bilingual_docs.pyrun by CI in xr-syntax). - What cannot be seen at a glance, such as why an input is built a certain way or where an expected result comes from, gets a bilingual comment.
- A LibXR test file starts with a comment stating what it tests and any special setup; comments in the body explain important steps and expected results, not obvious assignments and assertions.
Environment
- Tests depend on no network, machine paths, user git configuration or execution order, and write files into temporary directories.
- Git repositories in tests are created by the tests themselves with a fixed author and commit configuration (XRobot's
fixtures.run_git). - Python tests that need a C++ compiler select it through the
CXXenvironment variable and are skipped without one; CI runs them on Linux with both g++ and clang++. - Compilation checks of the same kind are combined into one compilation where possible.
Ablation
After tests are reorganized or a batch of tests is added, coverage and mutation testing confirm that every test is needed. The check is done by hand in the following steps:
- Record the lines each test executes with coverage's
dynamic_context = test_function. - Make one small change to an executed line: invert a comparison or a condition, swap
and/or, change a constant or a string, remove araiseor a call, or returnNone. For each change, run only the tests that executed that line and record which fail. Changes are located by syntax-tree position, so they run on a copy of a committed version (git archive), never on a working tree that is being edited; setPYTHONDONTWRITEBYTECODE=1, or Python reuses the.pycof the previous change when two changes of one file have the same size and are written within the same second. - A test whose detected changes are all detected by other tests as well is a candidate for removal or merging. Remove it only after confirming that it checks nothing the others do not check (for example the full error text or a compilation result).
- A change no test detects means a missing test or a missing assertion, which is then added; changes that do not affect results (for example to a cache, a thread count or garbage-collection settings) are recorded. Tests run in English and cannot see the Chinese text inside
tr(); dedicated tests sample the Chinese output per language instead of comparing every message.