To run the tests, you need to compile with the flag BUILD_TESTS
enabled (-DBUILD_TESTS:BOOL=ON
).
This will build all tests within the tests
directory and generate a tests
folder in the build directory, which contains the test executable.
Once the executable is generated, you can run the tests with the following command:
cd build/{build_type}/tests/unit
./canary_ut
cd build/{build_type}/tests/integration
./canary_it
You can also run the tests using CTest:
-- from the build/{build_type} directory
-- to run all tests
ctest --verbose
-- to run only unit tests
ctest --verbose -R unit
-- to run only integration tests
ctest --verbose -R integration
Tests are added in the tests
folder, in the root of the repository.
As much as possible, tests should be added in the same folder as the code they are testing:
- src
- foo
- foo.cpp
- bar
- bar.cpp
- tests
- foo
- foo_test.cpp
- bar
- bar_test.cpp
Tests are written using the Boost::ut framework. We've chosen this framework because it is simple, macro-free, intuitive, and easy to use. In this section, we will go over the basics of the framework, but you can find more information in the Boost::ut documentation. There are many ways to write tests, and we encourage you to read the documentation to find the way that works best for you.
Tests needs to be encapsulated by suites, which are defined using suite<>
structure.
suite<"foo"> test_foo = [] {
// Tests go here
};
This puts all tests within test_foo under the suite "foo". You can use declare multiple suites with the same name, and they will be merged together. This allows you to declare suites in different files, and they will be merged together, keeping test files clean.
Avoid creating tests directly in the main function, as this will put them in the global
suite, which is not recommended.
Tests can be defined using the test()
lambda or the _test
operator:
suite<"foo"> test_foo = [] {
"test 1"_test = [] {
// Test 1
};
test("test 2") = [] {
// Test 2
};
};
Both are equivalent, the received argument is the description of the test. The description is used to identify the test in the output, and should be unique within a suite.
Assertions are done using the expect()
function:
suite<"foo"> test_foo = [] {
"test 1"_test = [] {
expect(eq(1_i, 1_i));
};
};
It's always preferable to use comparison operators (eq, neq, etc.), as they provide better error messages.
Testing against injected dependencies is done using the di::extension::injector<>
class.
This class is used to create the dependency injection container, and can be used to set up the container for testing.
For example, to test the RSA
class, we need to inject a Logger
class, otherwise it will use the default SpdLog implementation.
suite<"security"> rsaTest = [] {
test("RSA::start logs error for missing .pem file") = [] {
di::extension::injector<> injector{};
DI::setTestContainer(&InMemoryLogger::install(injector));
DI::create<RSA &>().start();
auto &logger = dynamic_cast<InMemoryLogger &>(injector.create<Logger &>());
expect(eq(1, logger.logs.size()) >> fatal);
expect(
eq(std::string{"error"}, logger.logs[0].level) and
eq(std::string{"File key.pem not found or have problem on loading... Setting standard rsa key\n"}, logger.logs[0].message)
);
};
};