|
SpECTRE
v2026.04.01
|
Unit tests are placed in the appropriate subdirectory of tests/Unit, which mirrors the directory hierarchy of src. Typically there should be one test executable for each production code library. For example, we have a DataStructures library and a Test_DataStructures executable. When adding a new test there are several scenarios that can occur, which are outlined below.
All tests must start with
The file tests/Unit/Framework/TestingFramework.hpp must always be the first include in the test file and must be separated from the STL includes by a blank line. All classes and free functions should be in an anonymous/unnamed namespace, e.g.
This is necessary to avoid symbol redefinition errors during linking.
Test cases are added by using the SPECTRE_TEST_CASE macro. The first argument to the macro is the test name, e.g. "Unit.DataStructures.Tensor", and the second argument is a list of tags. The tags list is a string where each element is in square brackets. For example, "[Unit][DataStructures]". The tags should only be the type of test, in this case Unit, and the library being tested, in this case DataStructures. The SPECTRE_TEST_CASE macro should be treated as a function, which means that it should be followed by { /* test code */ }. For example,
From within a SPECTRE_TEST_CASE you are able to do all the things you would normally do in a C++ function, including calling other functions, setting variables, using lambdas, etc.
The CHECK macro in the above example is provided by Catch2 and is used to check conditions. We also provide the CHECK_ITERABLE_APPROX macro which checks if two doubles or two iterable containers of doubles are approximately equal. CHECK_ITERABLE_APPROX is especially useful for comparing Tensors, DataVectors, and Tensor<DataVector>s since it will iterate over nested containers as well.
All unit tests must finish within a few seconds, the hard limit is 5, but having unit tests that long is strongly discouraged. They should typically complete in less than half a second. Tests that are longer are often no longer testing a small enough unit of code and should either be split into several unit tests or moved to an integration test.
When you add a new test to a source file or rename an existing test the change needs to be discovered by the testing infrastructure. This is done by building the target rebuild_cache, e.g. by running make rebuild_cache.
Pointwise functions should generally be tested in two different ways. The first is by taking input from an analytic solution and checking that the computed result is correct. The second is to use the random number generation comparison with Python infrastructure. In this approach the C++ function being tested is re-implemented in Python and the results are compared. Please follow these guidelines:
It is possible to test C++ functions that return by value and ones that return by gsl::not_null. In the latter case, since it is possible to return multiple values, one Python function taking all non-gsl::not_null arguments must be supplied for each gsl::not_null argument to the C++. To perform the test the pypp::check_with_random_values() function must be called. For example, the following checks various C++ functions by calling into pypp:
The corresponding Python functions are:
Many tests in SpECTRE make use of randomly generated numbers in order to increase the parameter space covered by the tests. The random number generator is set up using:
The generator gen can then be passed to distribution classes such as std::uniform_real_distribution or UniformCustomDistribution.
Each time the test is run, a different random seed will be used. When writing a test that uses random values, it is good practice to run the test at least \(10^4\) times in order to set any tolerances on checks used in the test. This can be done by using the following command in the build directory (SPECTRE_BUILD_DIR):
where TEST_NAME is the test name passed to SPECTRE_TEST_CASE (e.g. Unit.Evolution.Systems.CurvedScalarWave.Characteristics).
If a test case fails when using a random number generated by MAKE_GENERATOR, as part of the output from the failed test will be the text
Note that the output of tests can be found in SPECTRE_BUILD_DIR/Testing/Temporary/LastTest.log
The failing test case can then be reproduced by changing MAKE_GENERATOR call at the provided line in the given file to
If the MAKE_GENERATOR is within CheckWithRandomValues.hpp, the failing test case most likely has occurred within a call to pypp::check_with_random_values(). In such a case, additional information should have been printed to help you determine which call to pypp::check_with_random_values() has failed. The critical information is the line
where FUNCTION_NAME should correspond to the third argument of a call to pypp::check_with_random_values(). The seed that caused the test to fail can then be passed as an additional argument to pypp::check_with_random_values(), where you may also need to pass in the default value of the comparison tolerance.
Typically, you will need to adjust a tolerance used in a CHECK somewhere in the test in order to get the test to succeed reliably. The function pypp::check_with_random_values() takes an argument that specifies the lower and upper bounds of random quantities. Typically these should be chosen to be of order unity in order to decrease the chance of occasionally generating large numbers through multiplications which can cause an error above a reasonable tolerance.
ASSERTs and ERRORs can be tested with the CHECK_THROWS_WITH macro. This macro takes two arguments: the first is either an expression or a lambda that is expected to trigger an exception (which now are thrown by ASSERT and ERROR (Note: You may need to add () wrapping the lambda in order for it to compile.); the second is a Catch Matcher (see Catch2 for complete documentation), usually a Catch::Matchers::ContainsSubstring() macro that matches a substring of the error message of the thrown exception.
When testing ASSERTs the CHECK_THROWS_WITH should be enclosed between #ifdef SPECTRE_DEBUG and an #endif If the #ifdef SPECTRE_DEBUG block is omitted then compilers will correctly flag the code as being unreachable which results in warnings.
Adding the "attribute" // [[OutputRegex, Regular expression to match]] before the SPECTRE_TEST_CASE macro will force ctest to only pass the particular test if the regular expression is found in the output of the test. In this case, the first line of the test should call the macro OUTPUT_TEST();.
The action testing framework is documented as part of the ActionTesting namespace.
We have a suite of input file tests in addition to unit tests. Every input file in the tests/InputFiles/ directory is added to the test suite automatically. If you don't want your input file tested at all, add the relative input file path to the whitelist in cmake/AddInputFileTests.cmake. If the input file is being tested, it must specify the Executable it should run with in the input file metadata (above the --- marker in the input file). Properties of the test are controlled by the Testing section in the input file metadata. The following properties are available: