LINUX.ORG.RU

μt — C++20 библиотека модульного тестирования

 , , , ,


0

2

μt¹ – небольшая (~100 Кб), header-only (единственный файл ut.hpp) C++20 библиотека модульного тестирования.
В отличие от большинства аналогов (сatch³, doctest⁴, etc.) основана не на макросах, а на возможностях стандарта C++20.
Библиотекой поддерживаются техники TDD (разработка через тестирование), BDD (разработка через поведение) и язык BDD Gherkin.
Зависит только от std.

Примеры:

#include <boost/ut.hpp> // import boost.ut;

constexpr auto sum(auto... values) { return (values + ...); }

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

  "sum"_test = [] {
    expect(sum(0) == 0_i);
    expect(sum(1, 2) == 3_i);
    expect(sum(1, 2) > 0_i and 41_i == sum(40, 2));
  };
}
#include <boost/ut.hpp>
#include <chrono>
#include <iostream>
#include <string>
#include <string_view>

namespace benchmark {
struct benchmark : boost::ut::detail::test {
  explicit benchmark(std::string_view _name)
      : boost::ut::detail::test{"benchmark", _name} {}

  template <class Test>
  auto operator=(Test _test) {
    static_cast<boost::ut::detail::test&>(*this) = [&_test, this] {
      const auto start = std::chrono::high_resolution_clock::now();
      _test();
      const auto stop = std::chrono::high_resolution_clock::now();
      const auto ns =
          std::chrono::duration_cast<std::chrono::nanoseconds>(stop - start);
      std::clog << "[" << name << "] " << ns.count() << " ns\n";
    };
  }
};

[[nodiscard]] auto operator""_benchmark(const char* _name,
                                        decltype(sizeof("")) size) {
  return ::benchmark::benchmark{{_name, size}};
}

#if defined(__GNUC__) or defined(__clang__)
template <class T>
void do_not_optimize(T&& t) {
  asm volatile("" ::"m"(t) : "memory");
}
#else
#pragma optimize("", off)
template <class T>
void do_not_optimize(T&& t) {
  reinterpret_cast<char volatile&>(t) =
      reinterpret_cast<char const volatile&>(t);
}
#pragma optimize("", on)
#endif
}  // namespace benchmark

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

  "string creation"_benchmark = [] {
    std::string created_string{"hello"};
    do_not_optimize(created_string);
  };
}

BDD:

#include <boost/ut.hpp>

int main() {
  using namespace boost::ut::literals;
  using namespace boost::ut::operators::terse;
  using namespace boost::ut::bdd;

  "Scenario"_test = [] {
    given("I have...") = [] {
      when("I run...") = [] {
        then("I should have...") = [] { 1_u == 1u; };
        then("I should have...") = [] { 1u == 1_u; };
      };
    };
  };

  feature("Calculator") = [] {
    scenario("Addition") = [] {
      given("I have number 40") = [] {
        auto number = 40;
        when("I add 2 to number") = [&number] { number += 2; };
        then("I expect number to be 42") = [&number] { 42_i == number; };
      };
    };
  };

  // clang-format off
  scenario("Addition");
    given("I have number 40");
      auto number = 40;

    when("I add 2 to number");
      number += 2;

    then("I expect number to be 42");
      42_i == number;
}

Gherkin:

#include <boost/ut.hpp>
#include <fstream>
#include <numeric>
#include <streambuf>
#include <string>

template <class T>
class calculator {
 public:
  auto enter(const T& value) -> void { values_.push_back(value); }
  auto add() -> void {
    result_ = std::accumulate(std::cbegin(values_), std::cend(values_), T{});
  }
  auto sub() -> void {
    result_ = std::accumulate(std::cbegin(values_) + 1, std::cend(values_),
                              values_.front(), std::minus{});
  }
  auto get() const -> T { return result_; }

 private:
  std::vector<T> values_{};
  T result_{};
};

int main(int argc, const char** argv) {
  using namespace boost::ut;

  bdd::gherkin::steps steps = [](auto& steps) {
    steps.feature("Calculator") = [&] {
      steps.scenario("*") = [&] {
        steps.given("I have calculator") = [&] {
          calculator<int> calc{};
          steps.when("I enter {value}") = [&](int value) { calc.enter(value); };
          steps.when("I press add") = [&] { calc.add(); };
          steps.when("I press sub") = [&] { calc.sub(); };
          steps.then("I expect {value}") = [&](int result) {
            expect(that % calc.get() == result);
          };
        };
      };
    };
  };

  // clang-format off
  "Calculator"_test = steps |
    R"(
      Feature: Calculator

        Scenario: Addition
          Given I have calculator
           When I enter 40
           When I enter 2
           When I press add
           Then I expect 42

        Scenario: Subtraction
          Given I have calculator
           When I enter 4
           When I enter 2
           When I press sub
           Then I expect 2
    )";
  // clang-format on

  if (argc == 2) {
    const auto file = [](const auto path) {
      std::ifstream file{path};
      return std::string{(std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>()};
    };

    "Calculator"_test = steps | file(argv[1]);
  }
}

  1. https://github.com/boost-ext/ut
  2. https://boost-ext.github.io/ut – примеры, документация, бенчмарки
  3. https://github.com/catchorg/Catch2
  4. https://github.com/doctest/doctest
  5. https://boost-ext.github.io/ut/denver-cpp-2019 – слайд-презентация.
  6. https://www.youtube.com/watch?v=irdgFyxOs_Y – презентация на CppCon 2020.
★★★★★

Последнее исправление: dataman (всего исправлений: 2)

Я хотел использовать, но сходу не нашел как её заставить выводить результат в формате junit xml. Это возможно?

Reset ★★★★★
()
 What about Mocks/Stubs/Fakes?
 Consider using one of the following frameworks
 https://github.com/cpp-testing/GUnit/blob/master/docs/GMock.md

Я так понял мокать оно не умеет и для замены gtest еще не годится?

Жаль, примеры с BDD неплохи.

PRN
()
Ответ на: комментарий от Reset

выводить результат в формате junit xml

«Из коробки» нет.

Это возможно?

Да, есть примеры использования cfg::runner, cfg::reporter, и cfg::printer.

dataman ★★★★★
() автор топика

Насколько сильно μt замедляет компиляцию проекта при подключении заголовочного файла? Например, Catch2 знатный тормоз, а тот же doctest особо не влияет на время сборки.

m0rph ★★★★★
()

Не раскрыта тема, чем она лучше гуглотестов и... раз уж пошла такая пьянка маркетингового минимализма - наколенных юнит-тестов, велосипедимых за 5 минут на ходу, средствами собственно сиплюсплюса :)

slackwarrior ★★★★★
()
Последнее исправление: slackwarrior (всего исправлений: 1)
Ответ на: комментарий от PRN

BDD — это прежде всего методология. Она в головах, а не в конструкциях :) Конструкции обычно отражают видение тараканов в голове автора. Это почти проективные тесты скорее авторов, как они понимают методологию (чаще всего «ритуально воспроизводя бестпрактисы»). Именно поэтому большинство «фреймворков»: а)перегружены абстрактной фигней, которая нужна 3.5 человекам из секты «истинных адептов правильных тестов» б) ограничены представлениями авторов о полезности конструкций для. 90 процентов людей использует некоторое «компактное подмножество». Особенно радуют конструкции с пересекающимся функционалом «для почти того же» (или соответствующие тому же самому в языках программирования).

А большинство любых тестов — это исходная диспозиция «дано», с предусловиями, и проверка сходимости постусловий :)

Например, когда автотестированием еще почти нигде не пахло, автоматизировали тестирование ПО для бирж на основе публично доступных исторических данных, тестерам предлагалась «оверинжиниреная» система автотестирования с конфигурацией на XML, в которой большую часть теста занимала простыня с конфигурацией... которая во многих тестах пересекается и не должна вообще копипаститься бы. Но авторы системы на это ложили болт, т.к. «они так видят», «тесты должны быть изолированы!» (поэтому восход солнца в каждом вручную), и смысловая часть теста терялась в простыне конфигурации. Долго ли коротко ли, этого монстра заменили на «пулемет» в виде эмулятора биржи для обстрела сервера сообщениями, на встроенном жабоскрипте с наколенной «библиотекой», сводящей тест к трем строчкам — сообщение, запрос, ответ. А не «сферический в вакууме манямирок» с фейками всего подряд. И даже тестеры, которые воротили нос от «работы программистов» и херачили проверки вручную по эксельке, поменяли мнение и нашли новую систему полезной.

DSL тестов же в большинстве случаев довольно тривиальный — в противном случае, когда он пытается быть слишком «обобщенным», он превращается в... еще один странный язык программирования, непротиворечивость(или хотя бы безбажность) и пользу которого как бэ могут оценить лишь «истинные адепты» (по сути каличные программисты на нем), которые, как специалисты подобные флюсу, чаще всего не слышали про теорему о неполноте и что строго что-то доказать в рамках самой системы они не могут примерно ничего (потому что теорем в процессе работы не доказывают, и доказательное программирование с формальной верификацией (с использованием хотя бы Coq) для них вообще что-то на непонятном) :)

slackwarrior ★★★★★
()
Последнее исправление: slackwarrior (всего исправлений: 1)
Ответ на: комментарий от Reset

как её заставить выводить результат в формате junit xml

Я ошибся, есть reporter_junit. Нужно добавлять --reporter junit в параметры бинарника.

$ ./boost_ut_hello_world --reporter junit

<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="all" tests="3" failures="0" time="0">
<testsuite classname="./boost_ut_hello_world" name="global" tests="3" errors="0" failures="0" skipped="0" time="0" version="201">
 <testcase classname="global" name="hello world" tests="3" errors="0" failures="0" skipped="0" time="0" status="PASSED">
  <system-out>

Running test "hello world"...
  </system-out>
 </testcase>
</testsuite>
</testsuites>

$ ./boost_ut_hello_world -h

./boost_ut_hello_world [<test name|pattern|tags> ... ] options

with options:
  -? -h --help                  display usage information
  -l --list-tests               list all/matching test cases
  -t, --list-tags               list all/matching tags
  -s, --success                 include successful tests in output
  -o, --out <filename>          output filename
  -r, --reporter <name>         reporter to use (defaults to console)
  -n, --name <name>             suite name
  -a, --abort                   abort at first failure
  -x, --abortx <no. failures>   abort after x failures
  -d, --durations               show test durations
  -D, --min-duration <seconds>  show test durations for [...]
  -f, --input-file <filename>   load test names to run from a file
  --list-test-names-only        list all/matching test cases names only
  --list-reporters              list all reporters
  --order <decl|lex|rand>       test case order (defaults to decl)
  --rng-seed <'time'|number>    set a specific seed for random numbers
  --use-colour <yes|no>         should output be colourised
  --libidentify                 report name and version according to libidentify standard
  --wait-for-keypress <never|start|exit|both>waits for a keypress before exiting

$ ./boost_ut_hello_world --list-reporters

available reporter:
  console (default)
  junit
dataman ★★★★★
() автор топика
Последнее исправление: dataman (всего исправлений: 1)
Ответ на: комментарий от slackwarrior

BDD — это прежде всего методология.

Все верно, но очевидно что забивать гвозди удобнее молотком, а не миксером.

т.к. «они так видят», «тесты должны быть изолированы!»

Это правильное утверждение, если которое воспринимать буквально будет то, что ты описал.

заменили на «пулемет» в виде эмулятора биржи для обстрела сервера сообщениями

Конфигурация эмулятора никуда из тестов не делась, могла упроститься, но исчезнуть она не могла. Иначе один тест портит сетап для другого теста. И именно поэтому они должны быть «изолированы».

…каличные…

Мне кажется тебя куда-то понесло)

…тесты …теорему о неполноте …Coq

Смешались люди, кони, паровозы)) Тебя точно понесло.

PRN
()
Последнее исправление: PRN (всего исправлений: 2)
Ответ на: комментарий от PRN

Конфигурация эмулятора никуда из тестов не делась

Делась :) Нет ее. С т.з. теста важнее суть запроса и суть отклика.

Иначе один тест портит сетап для другого теста.

Никак не портит, потому что это один и тот же эндпойнт, который не меняется за время жизни от SOD до EOD. И размазывать там манную кашу со «сценарием» и «ролями» кви-про-кво «как хакер я хочу поиметь сервер в такой позе»«как админ я хочу обломать юзеров с наступлением EOD» не надо. Нужно послать сообщение про надвигающийся EOD и получить отклик что «покайтесь, ибо грядет» OK, сервер гоинг даун райт ин тудей миднайт.

Наблюдаю с 2013-го как и зачем это вот «правильное BDD» c «Геркином и адептами» внедрялось. Да, многие девелоперы с тех пор себе кукумбер глубоко засунули, причем добровольно, не задаваясь вопросами «Why BDD sucks in practice?», но вопросы были еще тогда не только у меня

Смешались люди, кони, паровозы)) Тебя точно понесло.

Ничего не смешалось. Т.к. эти «методологии» — ребрендинг того что и так известно, и ничего нового, кроме ритуалов, не добавляет. Просто у 95% так называемых «автотестеров» на рыночке порешающем нет квалификации для формальной верификации, вот и заменяют ее этой стыдобой с «еще одной наколенной индуктивной теорией» над другой индуктивной теорией, которой является любая программа. Вместо собственно доказательств работоспособности — миллион отчетиков об автотестах, которые по сути являются программами... на которые тестов нет. Поэтому выпускают не «качественное ПО», а «это такое качество» (good enough software), не работающие программы, а «программные продукты» (ТМ)

slackwarrior ★★★★★
()
Последнее исправление: slackwarrior (всего исправлений: 1)
8 декабря 2024 г.

Начал прикручивать к своему проекту. Все очень сыро:

$ ./ut_tests.exe -s

Running test "addr"... PASSED
Running test "addr6"... PASSED
Running test "bad_addr"... PASSED
Running test "timespec"... PASSED
Running test "line_splitter"... PASSED
Running test "zero_copy_line_splitter"... PASSED
Running test "self_id"... PASSED
Running test "resolv_nameservers"... PASSED
Running test "listen"... PASSEDWARNING test 'listen' for test suite 'socket' already present

Running test "listen"... PASSEDWARNING test 'listen' for test suite 'socket' already present

Running test "listen"... PASSEDWARNING test 'listen' for test suite 'socket' already present

Running test "listen"... PASSEDSuite 'socket': all tests passed (0 asserts in 4 tests)
Suite 'basic': all tests passed (97441 asserts in 8 tests)
Suite 'global': all tests passed (0 asserts in 0 tests)

Параметризация типами вводит одинаковые тесты, из-за чего создаются warnings в выводе. Какие-то проблемы с переводами строк в выводе. Один тест у меня делает asserts в цикле, в итоговый junit xml это летит как 97441 прошедших тестов. Создается всегда suite global, даже если у меня нет в global scope тестов.

В общем, выглядит красиво, но пока неюзабельно, еще пилить и пилить.

Reset ★★★★★
()
Последнее исправление: Reset (всего исправлений: 1)
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.