Testing¶
While scanning modules, Maud
will detect and
register C++ unit tests.
(This can be disabled by setting BUILD_TESTING = OFF
.)
Unit testing is based on GTest, and many basic
concepts like suites of test cases are inherited whole.
Many other aspects of writing tests are simplified.
Instead of duplicating GTest’s documentation or explaining
unit tests from the ground up, this documentation will
assume familiarity and mostly describe the ways Maud
’s
usage differs.
Instead of defining test suites explicitly with classes,
one test suite is produced for each C++ source which includes
the special import declaration import test_
. Each test suite
is compiled into an executable target named test_.${STEM}
.
In a suite source file, two macros are included in the predefines
buffer (an explicit #include
is unnecessary):
test cases are defined with TEST_
,
and in a test case assertions are made with EXPECT_
.
GTest is added to the include path, so explicit
#include <gtest/gtest.h>
is always available if necessary.
import test_;
TEST_(basic) {
int three = 3, five = 5;
EXPECT_(three == five);
// ~/maud/.build/_maud/project_tests/unit testing/basics.cxx:12: Failure
// Expected: three == five
// Actual: 3 vs 5
EXPECT_(not three);
// ~/maud/.build/_maud/project_tests/unit testing/basics.cxx:14: Failure
// Expected: three
// to be false
// EXPECT_(...) is an expression contextually convertible to bool
if (not EXPECT_(&three != nullptr)) return;
EXPECT_(&three != nullptr) or [](std::ostream &os) {
// A lambda can hook expectation failure and add more context
};
// GMock's matchers are available
EXPECT_("hello world" >>= HasSubstr("llo"));
}
// To check a test body against multiple values, parameterize with a range.
TEST_(parameterized, {111, 234}) {
EXPECT_(parameter > 0);
}
// To instantiate the test body with multiple types, parameterize with a tuple.
TEST_(typed, 0, std::string("")) {
EXPECT_(parameter + parameter == parameter);
}
Unit test API¶
-
TEST_(case_name, parameters...)¶
[source] Defines and registers a test case with optional parameters.
- Parameters:
case_name – The test case’s name
parameters – Parameters with which to parameterize the test body
If no parameters are provided, a single simple test case is defined.
TEST_(basic) { // assertions etc... }
If parameters are provided, each is wrapped into a distinct test case using the same test body. In the scope of the test body, the parameter is declared as
If parameters are read from an initializer list or other range then this is analogous to a value parameterized test.
TEST_(value_parameterized, {2, 3, 47, 8191}) { EXPECT_(is_prime(parameter)); }
Parameters may also differ in type if they are read from a tuple, analogous to a type parameterized test.
TEST_(type_parameterized, 0, std::string("")) { EXPECT_(parameter + parameter == parameter); }
Each parameter is printed and incorporated into the test case’s total name along with case_name and the suite’s name to make it accessible to filtering.
-
EXPECT_(condition...)¶
[source] Checks its condition, producing a failure if it is falsy.
- Parameters:
condition – An expression which is expected to be truthy.
To provide more information about a failed expectation, the condition will be printed as part of the failure. If the condition is a comparison, each argument will be printed.
int three = 3, five = 5; EXPECT_(three == five); // ~/maud/.build/_maud/project_tests/unit testing/basics.cxx:12: Failure // Expected: three == five // Actual: 3 vs 5
EXPECT_(...) produces an expression rather than a statement. It is contextually convertible to
bool
, truthy iff the condition was truthy. If additional context needs to be added to a failed expectation, a lambda can be provided which will only be called if the expectation fails.EXPECT_(&a == &b) or [&](auto &os) { os << "Extra context: " << a << " vs " << b; };
Matchers can also be used to write an assertion with
EXPECT_
through use of an overloadedoperator>>=
. (All matchers provided by GMock are exported bytest_
and so are available in a test suite without an explicit#include
.)auto str = "hello world"; EXPECT_(str >>= HasSubstr("boo")); // ~/maud/.build/_maud/project_tests/unit testing/basics.cxx:21: Failure // Expected: str has substring "boo" // Argument was: "hello world"
-
template<typename Match, typename Description = DefaultDescription<Match>>
class Matcher¶
[source] Helper for constructing matchers from lambdas.
Matchers can be used with
EXPECT_
usingoperator>>=
.For example, to define a matcher which checks whether a number is even:
Matcher IsEven = [](auto n, std::ostream &os) { return (n % 2) == 0; };
To parameterize a matcher, define a function which returns a matcher with the parameters in closure:
auto IsDivisibleBy(auto divisor) { return Matcher{[=](auto n, std::ostream &os) { os << "where the remainder is " << (n % divisor); return (n % divisor) == 0; }}; }
On failed expectations, matchers output a description of the way matching failed. By default, this uses the
type_name<T>
of the match lambda (which is usually something unique but uninformative, like"$_1"
). Description of the matcher can be customized with another lambda:auto BarPlusBazEq(int n) { return Matcher{ .match = [=](Foo f, std::ostream &os) { return f.bar() + f.baz() == n; }, .description = [=](std::ostream &os, bool negated) { os << "bar() + baz()"; os << (negated ? " does not equal " : " equals ") << n; }, }; }
Frequently, a lambda is sufficient for constructing a custom matcher. However, it’s worth noting that defining a custom matcher class is not prohibitively complex.
-
template<typename T>
std::string const type_name¶
[source] A string representation of a type’s name.
By default this is a best effort demangling from type_info. This template can be specialized to override the default string.
template <> std::string const type_name<Set<int>> = "Selection";
Custom main()
¶
Each suite is linked to gtest_main
. Since that defines main
as a weak symbol, a custom main function can be written in a
test suite and it will override gtest_main
’s default.
To write a custom main function for all test executables,
write an interface unit with export module test_:main;
and
that will be linked to each test executable instead of gtest_main
.
Overriding test_
¶
If it is preferable to override test_
entirely (for
example to use a different test library like
Catch2
instead of GTest
), write an interface unit with
export module test_
and define the cmake function maud_add_test
:
maud_add_test(source_file out_target_name)
If defined, this function will be invoked on each source file which
declares import test_
, module test_
, or any partition of it.
If out_target_name
is set to a
target name,
the source file will be attached to it and imports automatically
processed as with other Maud
targets. For example, if you would
prefer to unit test with a minimal custom framework you could define
your own module test_
:
.test_.cxx
module; #include "my_test_framework.hxx" export module test_; export using my_test_framework::expect_equals; // ...
Then inject this into unit tests by defining maud_add_test
:
test_.cmake
function(maud_add_test source_file out_target_name) cmake_path(GET source_file STEM name) set(${out_target_name} "test_.${name}" PARENT_SCOPE) # Create a test executable and register it add_executable(test_.${name}) add_test(NAME test_.${name} COMMAND $<TARGET_FILE:test_.${name}>) # Link the unit test with test_.cxx: target_sources( test_.${name} PRIVATE FILE_SET module_providers TYPE CXX_MODULES BASE_DIRS ${CMAKE_SOURCE_DIR} FILES .test_.cxx ) endfunction()
Then this could be used in a unit test:
math.test.cxx
import test_; int main() { expect_equals(1 + 2, 3); }
Formatting test¶
By default, if clang-format is detected then a test will be added which asserts that files are formatted consistently:
$ ctest --build-config Debug --tests-regex formatted --output-on-failure
Test project ~/maud/.build
Start 4: check.clang-formatted
1/1 Test #4: check.clang-formatted ............***Failed 0.07 sec
Clang-formating 16 files
~/maud/in2.cxx:15:42: error: code should be clang-formatted [-Wclang-format-violations]
export void compile_in2(std::istream &is, std::ostream &os);
^
A fix
target will also be added which formats files in place:
$ ninja fix.clang-format
Since the set of files which should be formatted is not necessarily
identical to the set which should be compiled, the separate
glob(MAUD_CXX_FORMATTED_SOURCES)
configures which files clang-format
will be applied to. Since different
major versions of clang-format
will frequently have different and
backwards-incompatible behavior and parameter spaces, the target version must
be specified in a .clang-format
file:
# Versions: 18
BasedOnStyle: Google
ColumnLimit: 90