How to Write a New Unit Test
This project uses the Erbsland Unit Test system as its testing framework. This short guide shows how to add a new unit test.
File Structure
Unit tests go in
test/unittest/src
Test data belongs in
test/unittest/data
The framework and its documentation live in
test/unittest/erbsland-unittest
Basic Test Structure & Naming
The framework automatically registers test classes and methods—if you follow these naming rules:
Class names must end in
...Test
(e.g.,IntegerTest
)Filenames must match the class name and go in
test/unittest/src
(e.g.,IntegerTest.cpp
)You must add the file to
test/unittest/src/CMakeLists.txt
Test methods must be named
test...()
, returnvoid
, and take no parameters
Minimal Example
For a class Example
, create ExampleTest.cpp
like this:
// Copyright (c) 2025 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <erbsland/unittest/all.hpp>
TESTED_TARGETS(Example)
class ExampleTest : public el::UnitTest {
public:
Example example;
void testConstruction() {
example = Example{};
REQUIRE(example.empty());
}
};
You can optionally use TESTED_TARGETS(...)
or TAGS(...)
to add metadata for filtering and test selection.
Test Macros
Here’s what you can use to verify results:
REQUIRE(expr)
REQUIRE_FALSE(expr)
REQUIRE_THROWS(expr)
REQUIRE_THROWS_AS(exception_class, expr)
REQUIRE_NOTHROW(expr)
REQUIRE_EQUAL(actual, expected)
REQUIRE_NOT_EQUAL(a, b)
REQUIRE_LESS(a, b)
REQUIRE_LESS_EQUAL(a, b)
REQUIRE_GREATER(a, b)
REQUIRE_GREATER_EQUAL(a, b)
If you’re testing custom types, make sure they can be formatted using std::format
:
template <>
struct std::formatter<Example> : std::formatter<std::string> {
auto format(const Example &value, format_context& ctx) const {
return std::formatter<std::string>::format(value.toString(), ctx);
}
};
Add Extra Info on Test Failures
To make test failures easier to diagnose, you can either override additionalErrorMessages()
or use runWithContext()
for context-aware error reporting.
Option 1: additionalErrorMessages()
This method lets you return more details when a test fails—like the current state of the object under test:
class ExampleTest : public el::UnitTest {
public:
Example example;
auto additionalErrorMessages() -> std::string override {
try {
return std::format("example.state = {}", example.state);
} catch (...) {
return "Exception in additionalErrorMessages()";
}
}
void testConstruction() {
example = Example{};
REQUIRE(example.empty());
}
};
This message is shown only if a test fails.
Option 2: runWithContext(...)
Use this when testing multiple cases in a loop, to show which input failed:
class ExampleTest : public el::UnitTest {
public:
void testConstruction() {
struct TestData {
int value;
std::string expectedOutput;
};
const auto testData = std::vector<TestData>{
{-1, "bad"},
{0, "good"},
{1, "good"},
// ...
};
for (const auto &data : testData) {
runWithContext(SOURCE_LOCATION(), [&]() {
auto ex = Example(data.value);
std::string output;
REQUIRE_NOTHROW(output = ex.toString());
REQUIRE_EQUAL(output, data.expectedOutput);
}, [&]() -> std::string {
return std::format("Failed for value = {}", data.value);
});
}
}
};