Skip to content
[Boost].UT: C++20 μ(micro)/Unit Testing Framework
C++ CMake
Branch: master
Clone or download

README.md

Boost Licence Version Build Status Build Status Coveralls Codacy Badge Github Issues Try it online

[Boost].UT / μt

C++20 single header/single module, macro-free μ(micro)/Unit Testing Framework

Testing

"If you liked it then you should have put a test on it", Beyonce rule

Quick start

Get the latest latest header/module here!

C++ header C++20 module
#include <boost/ut.hpp> import boost.ut;

Hello World (https://godbolt.org/z/pt-yUa)

constexpr auto sum = [](auto... args) { return (0 + ... + args); };
int main() {
  using namespace boost::ut;

  "hello world"_test = [] {
    expect(42_i == sum(40, 2));
  };
}
All tests passed (1 assert in 1 test)

Assertions (https://godbolt.org/z/pVk2M4)

"operators"_test = [] {
  expect(0_i == sum());
  expect(2_i != sum(1, 2));
  expect(sum(1) >= 0_i);
  expect(sum(1) <= 1_i);
};

"message"_test = [] {
  expect(3_i == sum(1, 2)) << "wrong sum";
};

"expressions"_test = [] {
  expect(0_i == sum() and 42_i == sum(40, 2));
  expect(0_i == sum() or 1_i == sum()) << "compound";
};

"that"_test = [] {
  expect(that % 0 == sum());
  expect(that % 42 == sum(40, 2) and that % (1 + 2) == sum(1, 2));
  expect(that % 1 != 2 or 2_i > 3);
};

"eq/neq/gt/ge/lt/le"_test = [] {
  expect(eq(42, sum(40, 2)));
  expect(neq(1, 2));
  expect(eq(sum(1), 1) and neq(sum(1, 2), 2));
  expect(eq(1, 1) and that % 1 == 1 and 1_i == 1);
};

"floating points"_test = [] {
  expect(42.1_d == 42.101) << "epsilon=0.1";
  expect(42.10_d == 42.101) << "epsilon=0.01";
  expect(42.10000001 == 42.1_d) << "epsilon=0.1";
};

"constant"_test = [] {
  constexpr auto compile_time_v = 42;
  auto run_time_v = 99;
  expect(constant<42_i == compile_time_v> and run_time_v == 99_i);
};

"fatal"_test = [] {
  std::vector v{1, 2, 3};
  !expect(std::size(v) == 3_ul) << "fatal assertion";
   expect(v[0] == 1_i);
   expect(v[1] == 2_i);
   expect(v[2] == 3_i);
};

"failure"_test = [] {
  expect(1_i == 2) << "should fail";
  expect(sum() == 1_i or 2_i == sum()) << "sum?";
};
Running "operators"...✔️
Running "message"...✔️
Running "expressions"...✔️
Running "that"...✔️
Running "eq/neq/gt/ge/lt/le"...✔️
Running "floating points"...✔️
Running "constant"...✔️
Running "fatal"...✔️
Running "failure"...
  assertions.cpp:61:FAILED [1 == 2] should fail
  assertions.cpp:62:FAILED [(0 == 1 or 2 == 0)] sum?
❌

===============================================================================

tests:   9  | 1 failed
asserts: 24 | 22 passed | 2 failed

Sections (https://godbolt.org/z/qKxsf9)

"[vector]"_test = [] {
  std::vector<int> v(5);

  !expect(5_ul == std::size(v));

  "resize bigger"_test = [=]() mutable {
    v.resize(10);
    expect(10_ul == std::size(v));
  };

  !expect(5_ul == std::size(v));

  "resize smaller"_test = [=]() mutable {
    v.resize(0);
    expect(0_ul == std::size(v));
  };
};
All tests passed (4 asserts in 1 tests)

Exceptions/Aborts (https://godbolt.org/z/A2EehK)

"exceptions/aborts"_test = [] {
  expect(throws<std::runtime_error>([]{throw std::runtime_error{""};}))
    << "throws runtime_error";
  expect(throws([]{throw 0;})) << "throws any exception";
  expect(nothrow([]{})) << "doesn't throw";
  expect(aborts([] { assert(false); }));
};
All tests passed (4 asserts in 1 tests)

Parameterized (https://godbolt.org/z/WCqggN)

"args"_test =
   [](const auto& arg) {
      expect(arg >= 1_i);
    }
  | std::vector{1, 2, 3};

"types"_test =
    []<class T> {
      expect(std::is_integral_v<T>) << "all types are integrals";
    }
  | std::tuple<bool, int>{};

"args and types"_test =
    []<class TArg>(const TArg& arg) {
      !expect(std::is_integral_v<TArg>);
       expect(42_i == arg or true_b == arg);
       expect(type<TArg> == type<int> or type<TArg> == type<bool>);
    }
  | std::tuple{true, 42};
All tests passed (11 asserts in 7 tests)

Logging (https://godbolt.org/z/26fPSY)

"logging"_test = [] {
  log << "pre";
  expect(42_i == 43) << "message on failure";
  log << "post";
};
Running "logging"...
pre
  logging.cpp:8:FAILED [42 == 43] message on failure
post
FAILED

===============================================================================

tests:   1 | 1 failed
asserts: 1 | 0 passed | 1 failed

Behavior Driven Development (https://godbolt.org/z/5nhdyn)

"scenario"_test = [] {
  given("I have...") = [] {
    when("I run...") = [] {
      then("I expect...") = [] { expect(1_i == 1); };
      then("I expect...") = [] { expect(1 == 1_i); };
    };
  };
};
All tests passed (2 asserts in 1 tests)

Test Suites (https://godbolt.org/z/CFbTP9)

namespace ut = boost::ut;

ut::suite _ = [] {
  using namespace ut;

  "equality/exceptions"_test = [] {
    "should equal"_test = [] {
      expect(42_i == 42);
    };

    "should throw"_test = [] {
      expect(throws([]{throw 0;}));
    };
  };
};

int main() { }
All tests passed (2 asserts in 1 tests)

Skipping tests (https://godbolt.org/z/R3dKAV)

skip | "don't run"_test = [] {
  expect(42_i == 43) << "should not fire!";
};
All tests passed (0 asserts in 0 tests)
1 tests skipped

Module (https://wandbox.org/permlink/CyqQu6PgVR1KvQpg)

import boost.ut;

int main() {
  using namespace boost::ut;

  "module"_test = [] {
    expect(42_i == 42);
  };
}
All tests passed (1 asserts in 1 tests)

Runner (https://godbolt.org/z/jdg687)

namespace ut = boost::ut;

namespace cfg {
class runner {
 public:
  /**
   * @example cfg<override> = { .filter = "test.section.*", .dry_run = true };
   * @param options.filter runs all tests which names matches test.section.* filter
   * @param options.dry_run if true then print test names to be executed without running them
   */
  auto operator=(options);

  /**
   * @example suite _ = [] {};
   * @param suite() executes suite
   */
  template<class TSuite>
  auto on(ut::events::suite<TSuite>);

  /**
   * @example "name"_test = [] {};
   * @param test.type ["test", "given", "when", "then"]
   * @param test.name "name"
   * @param test.arg parameterized argument
   * @param test() executes test
   */
  template<class... Ts>
  auto on(ut::events::test<Ts...>);

  /**
   * @example skip | "don't run"_test = []{};
   * @param skip.type ["test", "given", "when", "then"]
   * @param skip.name "don't run"
   * @param skip.arg parameterized argument
   */
  template<class... Ts>
  auto on(ut::events::skip<Ts...>);

  /**
   * @example file.cpp:42: expect(42_i == 42);
   * @param assertion.location { "file.cpp", 42 }
   * @param assertion.expr 42_i == 42
   * @return true if expr passes, false otherwise
   */
  template <class TLocation, class TExpr>
  auto on(ut::events::assertion<TLocation, TExpr>) -> bool;

  /**
   * @example !expect(2_i == 1)
   * @note triggered by `!expect`
   *       should std::exit
   */
  auto on(ut::events::fatal_assertion);

  /**
   * @example log << "message"
   * @param log.msg "message"
   */
  template<class TMsg>
  auto on(ut::events::log<TMsg>);
};
} // namespace cfg

template<> auto ut::cfg<ut::override> = cfg::runner{};

Reporter (https://godbolt.org/z/gsAPKg)

namespace ut = boost::ut;

namespace cfg {
class reporter {
 public:
  /**
   * @example "name"_test = [] {};
   * @param test_begin.type ["test", "given", "when", "then"]
   * @param test_begin.name "name"
   */
  auto on(ut::events::test_begin) -> void;

  /**
   * @example "name"_test = [] {};
   * @param test_run.type ["test", "given", "when", "then"]
   * @param test_run.name "name"
   */
  auto on(ut::events::test_run) -> void;

  /**
   * @example "name"_test = [] {};
   * @param test_skip.type ["test", "given", "when", "then"]
   * @param test_skip.name "name"
   */
  auto on(ut::events::test_skip) -> void;

  /**
   * @example "name"_test = [] {};
   * @param test_end.type ["test", "given", "when", "then"]
   * @param test_end.name "name"
   */
  auto on(ut::events::test_end) -> void;

  /**
   * @example log << "message"
   * @param log.msg "message"
   */
  template<class TMsg>
  auto on(ut::events::log<TMsg>) -> void;

  /**
   * @example file.cpp:42: expect(42_i == 42);
   * @param assertion_pass.location { "file.cpp", 42 }
   * @param assertion_pass.expr 42_i == 42
   */
  template <class TLocation, class TExpr>
  auto on(ut::events::assertion_pass<TLocation, TExpr>) -> void;

  /**
   * @example file.cpp:42: expect(42_i != 42);
   * @param assertion_fail.location { "file.cpp", 42 }
   * @param assertion_fail.expr 42_i != 42
   */
  template <class TLocation, class TExpr>
  auto on(ut::events::assertion_fail<TLocation, TExpr>) -> void;

  /**
   * @example !expect(2_i == 1)
   * @note triggered by `!expect`
   *       should std::exit
   */
  auto on(ut::events::fatal_assertion) -> void;

  /**
   * @example "exception"_test = [] { throw std::runtime_error{""}; };
   */
  auto on(ut::events::exception) -> void;

  /**
   * @note triggered on destruction of runner
   */
  auto on(ut::events::summary) -> void;
};
}  // namespace cfg

template <>
auto ut::cfg<ut::override> = ut::runner<cfg::reporter>{};

API

export module boost.ut;

namespace boost::ut::inline v1_1_1 {
  /**
   * Represents test suite object
   */
  struct suite final {
    /**
     * Creates and executes test suite
     * @example suite _ = [] {};
     * @param suite test suite function
     */
    constexpr explicit(false) suite(auto suite);
  };

  /**
   * Creates a test
   * @example "test name"_test = [] {};
   * @return test object to be executed
   */
  constexpr auto operator""_test;

  /**
   * Behaviour Driven Development (BDD) helper functions
   * @param name step name
   * @return test object to be executed
   */
  constexpr auto given = [](auto name);
  constexpr auto when  = [](auto name);
  constexpr auto then  = [](auto name);

  /**
   * Evaluates an expression
   * @example expect(42 == 42_i and 1 != 2_i);
   * @param expr expression to be evaluated
   * @param location [source code location](https://en.cppreference.com/w/cpp/utility/source_location))
   * @return stream
   */
  constexpr OStream& expect(
    Expression expr,
    const std::source_location& location = std::source_location::current()
  );

  inline namespace literals {
    /**
     * User defined literals to represent constant values
     * @example 42_i, 0_uc, 1.23_d
     */
    constexpr auto operator""_i;  /// int
    constexpr auto operator""_s;  /// short
    constexpr auto operator""_c;  /// char
    constexpr auto operator""_l;  /// long
    constexpr auto operator""_ll; /// long long
    constexpr auto operator""_u;  /// unsigned
    constexpr auto operator""_uc; /// unsigned char
    constexpr auto operator""_us; /// unsigned short
    constexpr auto operator""_ul; /// unsigned long
    constexpr auto operator""_f;  /// float
    constexpr auto operator""_d;  /// double
    constexpr auto operator""_ld; /// long double

    /**
     * Logical representation of constant values
     */
    constexpr auto true_b;        /// true
    constexpr auto false_b;       /// false
  } // namespace literals

  inline namespace operators {
    /**
     * Overloaded comparison operators to be used in expressions
     * @example (42_i != 0)
     */
    constexpr auto operator==;
    constexpr auto operator!=;
    constexpr auto operator>;
    constexpr auto operator>=;
    constexpr auto operator<;
    constexpr auto operator<=;

    /**
     * Overloaded logic operators to be used in expressions
     * @example (42_i != 0 and 1 == 2_i)
     */
    constexpr auto operator and;
    constexpr auto operator or;
    constexpr auto operator not;

    /**
     * Executes parameterized tests
     * @example "parameterized"_test = [](auto arg) {} | std::tuple{1, 2, 3};
     */
    constexpr auto operator|;
  } // namespace operators
} // namespace boost::ut

Configuration

Option Description Example
BOOST_UT_VERSION Current version 1'1'1
BOOST_UT_FORWARD Optionally used in .cpp files to speed up compilation of multiple test suites
BOOST_UT_IMPLEMENTATION Optionally used in main.cpp file to provide ut implementation (have to be used in combination with BOOST_UT_FORWARD)

How does it work?

suite

/**
 * Reperesents suite object
 * @example suite _ = []{};
 */
struct suite final {
  /**
   * Assigns and executes test suite
   */
  [[nodiscard]] constexpr explicit(false) suite(Suite suite) {
    suite();
  }
};

test

/**
 * Creates named test object
 * @example "hello world"_test
 * @return test object
 */
[[nodiscard]] constexpr Test operator ""_test(const char* name, std::size_t size) {
  return test{{name, size}};
}
/**
 * Represents test object
 */
struct test final {
  std::string_view name{}; /// test case name

  /**
   * Assigns and executes test function
   * @param test function
   */
  constexpr auto operator=(const Test& test) {
    std::cout << "Running... " << name << '\n';
    test();
  }
};

expect

/**
 * Evaluates an expression
 * @example expect(42_i == 42);
 * @param expr expression to be evaluated
 * @param location [source code location](https://en.cppreference.com/w/cpp/utility/source_location))
 * @return stream
 */
constexpr OStream& expect(
  Expression expr,
  const std::source_location& location = std::source_location::current()
) {
  if (not static_cast<bool>(expr) {
    std::cerr << location.file()
              << ':'
              << location.line()
              << ":FAILED: "
              << expr
              << '\n';
  }

  return std::cerr;
}
/**
 * Creates constant object for which operators can be overloaded
 * @example 42_i
 * @return integral constant object
 */
template <char... Cs>
[[nodiscard]] constexpr Operator operator""_i() -> integral_constant<int, value<Cs...>>;
/**
 * Overloads comparison if at least one of {lhs, rhs} is an Operator
 * @example (42_i == 42)
 * @param lhs Left-hand side operator
 * @param rhs Right-hand side operator
 * @return Comparison object
 */
[[nodiscard]] constexpr auto operator==(Operator lhs, Operator rhs) {
  return eq{lhs, rhs};
}
/**
 * Comparison Operator
 */
template <class TLhs, class TRhs>
struct eq final {
  TLhs lhs{}; // Left-hand side operator
  TRhs rhs{}; // Right-hand side operator

  /**
   * Performs comparison operatation
   * @return true if expression is succesful
   */
  [[nodiscard]] constexpr explicit operator bool() const {
    return lhs == rhs;
  }

  /**
   * Nicely prints the operation
   */
  friend auto operator<<(OStream& os, const eq& op) -> Ostream& {
    return (os << op.lhs << " == " << op.rhs);
  }
};

Benchmarks (https://github.com/cpp-testing/ut-benchmark)

Framework Version Standard License Linkage Test configuration
GoogleTest 1.10.0 C++11 BSD-3 library static library
Catch 2.10.2 C++11 Boost 1.0 single header CATCH_CONFIG_FAST_COMPILE
Doctest 2.3.5 C++11 MIT single header DOCTEST_CONFIG_SUPER_FAST_ASSERTS
μt 1.1.0 C++20 Boost 1.0 single header/module
Include / 0 tests, 0 asserts, 1 cpp file
Assert / 1 test, 1'000'000 asserts, 1 cpp file
Test / 1'000 tests, 0 asserts, 1 cpp file
Suite / 10'000 tests, 0 asserts, 100 cpp files
Suite+Assert / 10'000 tests, 40'000 asserts, 100 cpp files
Suite+Assert+STL / 10'000 tests, 20'000 asserts, 100 cpp files
Suite+Assert+STL / 10'000 tests, 20'000 asserts, 100 cpp files
(Headers vs Precompiled headers vs C++20 Modules)

Disclaimer [Boost].UT is not an official Boost library.

You can’t perform that action at this time.