UT (μt)

repository·master·Indexed 23 days ago

https://github.com/boost-ext/ut

A modern, macro-free C++20 unit testing framework designed for simplicity and speed. It is available as a single-header or single-module library and features automatic test registration, BDD/Gherkin support, spec-style testing (describe/it), and parameterized tests. It provides flexible assertion syntaxes including UDL, terse notation, and matchers, along with support for test suites, sections, and exception testing.

Tokens
7K
Snippets
22
Records
33
Agent score
31%

What's inside boost-ext-ut

  1. Customize the test reporter

    master

    To change how test results are displayed (e.g., changing the output format), specialize ut::cfg<ut::override> with a custom reporter. The reporter responds to events like test_begin, test_end, assertion_pass, assertion_fail, and summary.

    namespace ut = boost::ut;
    
    namespace cfg {
      class reporter {
       public:
        auto on(ut::events::test_begin) -> void {}
        auto on(ut::events::test_run) -> void {}
        auto on(ut::events::test_skip) -> void {}
        auto on(ut::events::test_end) -> void {}
        template <class TMsg> auto on(ut::events::log<TMsg>) -> void {}
        template <class TExpr> auto on(ut::events::assertion_pass<TExpr>) -> void {}
        template <class TExpr> auto on(ut::events::assertion_fail<TExpr>) -> void {}
        auto on(ut::events::fatal_assertion) -> void {}
        auto on(ut::events::exception) -> void {}
        auto on(ut::events::summary) -> void {}
      };
    }
    
    template <> auto ut::cfg<ut::override> = ut::runner<cfg::reporter>{};
  2. Customize the printer

    master

    You can provide a custom printer by inheriting from ut::printer and overloading the operator<<. This is useful for redirecting output to different streams like std::cerr or custom logging frameworks.

    namespace ut = boost::ut;
    
    namespace cfg {
    struct printer : ut::printer {
      template <class T>
      auto& operator<<(T&& t) {
        std::cerr << std::forward<T>(t);
        return *this;
      };
    };
    }
    
    template <> auto ut::cfg<ut::override> = ut::runner<ut::reporter<cfg::printer>>{};
  3. How UT works: Mental Model

    master

    UT is designed around a few core abstractions that compose to form a test runner:

    1. _test Literal: Creates a test object containing a name and a function.
    2. test Object: When assigned a function via operator=, it executes that function.
    3. expect: Evaluates an expression. It uses std::source_location to report exactly where a failure occurred.
    4. suite: A container that executes a collection of tests.
    5. Operators & _i: To allow expect to work with complex expressions (like ==) while maintaining type safety and performance, UT uses integral constant objects (created via _i) to overload comparison operators.
  4. Customize the test runner

    master

    You can override the default test runner by specializing ut::cfg<ut::override>. The runner handles events such as test execution, skipping, assertions, and logging. Implement the on method for the specific events you wish to intercept.

    namespace ut = boost::ut;
    
    namespace cfg {
      class runner {
       public:
        template <class... Ts> auto on(ut::events::test<Ts...> test) { test(); }
        template <class... Ts> auto on(ut::events::skip<Ts...>) {}
        template <class TExpr> auto on(ut::events::assertion<TExpr>) -> bool { return true; }
        auto on(ut::events::fatal_assertion) {}
        template <class TMsg> auto on(ut::events::log<TMsg>) {}
      };
    }
    
    template<> auto ut::cfg<ut::override> = cfg::runner{};
  5. Run, skip, and tag tests

    master

    UT provides several ways to define and control test execution:

    • UDL (User Defined Literals): Use the "name"_test syntax to define tests.
    • test() function: Use test("name") for standard test definitions.
    • Skipping: Prefix a test with skip / to prevent it from running.
    • Tagging: Use tag("name") / to categorize tests. You can then filter execution using cfg<override> = {.tag = {"tag_name"}};.

    Example of skipping and tagging:

    #include <boost/ut.hpp>
    
    int main() {
        using namespace boost::ut;
        // use ""_test
        "run UDL"_test = [] {
            expect(42_i == 42);
        };
        skip / "don't run UDL"_test = [] {
            expect(42_i == 43) << "should not fire!";
        };
        // test()
        test("run function") = [] {
            expect(42_i == 42);
        };
        skip / test("don't run function") = [] {
            expect(42_i == 43) << "should not fire!";
        };
    }
    
    // Tagging example
    tag("nightly") / tag("slow") /
    "performance"_test= [] {
      expect(42_i == 42);
    };
    
    // Configuration to run only specific tags
    cfg<override> = {.tag = {"nightly"}};
  6. Parameterized tests

    master

    UT allows running the same test logic against multiple inputs using two primary methods:

    1. for loop: Standard C++ loop to call test() or "name"_test multiple times.
    2. operator| syntax: Pipe a lambda into a collection (like std::vector or std::tuple).

    Key Features:

    • Automatic Naming: When using operator|, UT automatically extends the test name with parameter values (for integral/floating point types) or parameter indices (for complex types) to avoid duplicates.
    • Type Parameterization: You can parameterize over types using std::tuple<T...>.
    • Custom Formatting: You can overload format_test_parameter to control how non-integral types appear in test names.
    • Type Reflection: Use reflection::type_name<T>() to include the type name in failure messages.
    #include <vector>
    #include <tuple>
    #include <type_traits>
    #include <boost/ut.hpp>
    
    int main() {
        using namespace boost::ut;
    
    // Method 1: for loop
    for (const auto& i : std::vector{ 1, 2, 3 }) {
            test("parameterized " + std::to_string(i)) = [i] {
                expect(that % i > 0);
            };
        }
    
    // Method 2: operator|
    "args"_test =
            [](const auto& arg) {
            expect(arg >= 1_i);
            }
        | std::vector{ 1, 2, 3 };
    
    // Type parameterization
    "types"_test =
            []<class T>() {
            expect(std::is_integral_v<T>);
        }
        | std::tuple<bool, int>{};
    
    // Combined args and types
    "args and types"_test =
            []<class TArg>(const TArg & arg) {
            expect(std::is_integral_v<TArg> >> fatal);
            expect(42_i == static_cast<int>(arg) or arg);
            expect(type<TArg> == type<int> or type<TArg> == type<bool>);
            expect(type<TArg> == type<int> or type<TArg> == type<bool>);
        }
        | std::tuple{ true, 42 };
    
    // Using type names in failures
    "types with type name"_test =
            []<class T>() {
            expect(std::is_unsigned_v<T>) << reflection::type_name<T>() << "is unsigned";
        }
        | std::tuple<unsigned int, float>{};
    }
  7. Gherkin-style BDD testing

    master

    For high-level specification, UT provides a Gherkin implementation. You define steps using a lambda and then pipe a Gherkin-formatted string into the test definition.

    #include <boost/ut.hpp>
    
    int main() {
        using namespace boost::ut;
    
    bdd::gherkin::steps steps = [](auto& steps) {
            steps.feature("*") = [&] {
                steps.scenario("*") = [&] {
                    steps.given("I have a number {value}") = [&](int value) {
                        auto number = value;
                        steps.when("I add {value} to it") = [&](int value) {
                            number += value;
                        };
                        steps.then("I expect number to be {value}") = [&](int value) {
                            expect(that % number == value);
                        };
                    };
                };
            };
        };
    
    "Gherkin"_test = steps |
            R"(
          Feature: Number
            Scenario: Addition
              Given I have a number 40
               When I add 2 to it
               Then I expect number to be 42
        )";
    }
  8. Write your first assertion with expect()

    master

    The primary way to perform assertions in UT is using the expect() function.

    By default, expect(condition) will report a failure if the condition is false but allow the test to continue. To make an assertion fatal (terminating the test immediately upon failure), wrap the condition in fatal().

    To ensure that failed expressions are printed clearly in the output (e.g., 1 == 2 instead of just false), use the User Defined Literal (UDL) _i for integer constants. This allows UT to override comparison operators and prevents accidental type mismatches.

    #include <boost/ut.hpp>
    
    int main() {
        using namespace boost::ut;
    
        // Standard assertion
        expect(1_i == 2);
    
        // Fatal assertion (stops execution on failure)
        expect(fatal(1 == 2_i));
        expect(1_i == 2); // This line will not be executed if the previous one fails
    }
  9. Logging with boost::ut::log

    master

    UT provides a log utility for printing information during test execution.

    • Stream-based logging: Use boost::ut::log << ... for standard stream output.
    • Formatting-based logging: If using C++20 with std::format support, you can use boost::ut::log("format string", args...) for more advanced formatting.
    #include <boost/ut.hpp>
    
    int main() {
        using namespace boost::ut;
    
    // Stream logging
    "logging"_test = [] {
            boost::ut::log << "pre";
            expect(42_i == 43) << "message on failure";
            boost::ut::log << "post";
        };
    
    // Formatting logging (C++20)
    "logging_format"_test = [] {
            boost::ut::log("\npre  {} == {}", 42, 43);
            expect(42_i == 43) << "message on failure";
            boost::ut::log("\npost {} == {} -> {}", 42, 43, 42 == 43);
        };
    }
  10. Install and integrate UT

    master

    UT is a single-header or single-module C++20 unit testing framework. You can integrate it using several methods:

    1. Manual Integration

    Download the latest boost/ut.hpp (header) or boost/ut.cppm (module) and include/import it in your project.

    2. CMake Integration

    If you have installed UT via CMake, use find_package to import the Boost::ut target. Linking against this target automatically handles include directories.

    3. Conan Integration

    The boost-ext-ut package is available on Conan Center. Add boost-ext-ut/2.3.1 to your conanfile.

    4. Build and Install from Source

    cmake -Bbuild -H.
    cd build && make         # run tests
    cd build && make install # install
    find_package(ut REQUIRED)
    add_library(my_test my_test.cpp)
    target_link_libraries(my_test PRIVATE Boost::ut)
  11. Organize tests into Suites

    master

    You can group tests into named suites by defining a lambda in a specific namespace or variable. This is useful for separating error-handling tests or specific module tests.

    #include <boost/ut.hpp>
    
    namespace ut = boost::ut;
    
    ut::suite errors = [] {
        using namespace ut;
    
    "throws"_test = [] {
            expect(throws([] { throw 0; }));
        };
        "doesn't throw"_test = [] {
            expect(nothrow([] {}));
        };
    };
    
    int main() {}
  12. Behavior Driven Development (BDD) with Given/When/Then

    master

    UT supports BDD style testing using given, when, and then blocks to describe test scenarios clearly.

    #include <boost/ut.hpp>
    
    int main() {
        using namespace boost::ut;
        using namespace boost::ut::bdd;
    
    "scenario"_test = [] {
            given("I have...") = [] {
                when("I run...") = [] {
                    then("I expect 1...") = [] { expect(1_i == 1); };
                    then("I expect 2...") = [] { expect(1 == 1_i); };
                };
            };
        };
    }