Skip to content

Latest commit

 

History

History
188 lines (143 loc) · 5.9 KB

testing.rst

File metadata and controls

188 lines (143 loc) · 5.9 KB

Unit tests

While scanning modules, Maud will detect and :cmake:`register <command/add_test.html>` C++ unit tests. (This can be disabled by setting BUILD_TESTING = OFF.) Unit testing is based on :gtest:`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 :c:macro:`TEST_`, and in a test case assertions are made with :c:macro:`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

.. trike-macro:: TEST_(case_name, parameters...)

.. trike-macro:: EXPECT_(condition...)

.. cpp:module:: test_

.. trike-class:: template <typename Match, \
                           typename Description = DefaultDescription<Match>> \
                 Matcher

.. trike-var:: template <typename T> std::string const type_name

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 :mastering-cmake:`target name <Key Concepts.html#targets>`, 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 ClangFormat 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 target will also be added which formats files in place:

$ ninja -C fix.clang-format

Since the set of files which should be formatted is not necessarily identical to the set which should be compiled, a separate glob must be provided in the project's root .clang-format file:

# Maud: {
#   "version": 18,
#   "patterns": [
#     "\\.[ch]xx$",
#     "!rapidyaml\\.hxx"
#   ]
# }
BasedOnStyle: Google
ColumnLimit: 90