Тесты на C++ без макросов и динамической памяти

Многие популярные библиотеки для тестирования, например Google Test, Catch2, Boost.Test тяжело завязаны на использование макросов, так что в качестве примера тестов на этих библиотеках вы обычно увидите картину вроде такой:


namespace {

// Tests the default c'tor.
TEST(MyString, DefaultConstructor) {
  const MyString s;
  EXPECT_STREQ(nullptr, s.c_string());
  EXPECT_EQ(0u, s.Length());
}

const char kHelloString[] = "Hello, world!";

// Tests the c'tor that accepts a C string.
TEST(MyString, ConstructorFromCString) {
  const MyString s(kHelloString);
  EXPECT_EQ(0, strcmp(s.c_string(), kHelloString));
  EXPECT_EQ(sizeof(kHelloString)/sizeof(kHelloString[0]) - 1,
            s.Length());
}

// Tests the copy c'tor.
TEST(MyString, CopyConstructor) {
  const MyString s1(kHelloString);
  const MyString s2 = s1;
  EXPECT_EQ(0, strcmp(s2.c_string(), kHelloString));
}
}  // namespace

К макросам в C++ отношение настороженное, почему же они так процветают в библиотеках для создания тестов?


Библиотека юнит-тестов должна предоставить её пользователям способ написания тестов, так чтобы среда выполнения тестов могла их как-то найти и выполнить. Когда вы подумаете о том как это сделать, то использование макросов кажется кажется проще всего. Макрос TEST() обычно как-то определяет функцию (в случае с Google Test макрос также создает класс) и обеспечивает попадание адреса этой функции в какой-нибудь глобальный контейнер.


Хорошо известная мне библиотека, в которой реализован подход без единого макроса, это tut-framework. Посмотрим её пример из туториала:


#include <tut/tut.hpp>

namespace tut
{
    struct basic{};
    typedef test_group<basic> factory;
    typedef factory::object object;
}

namespace
{
    tut::factory tf("basic test");
}

namespace tut
{
    template<> template<>
    void object::test<1>()
    {
        ensure_equals("2+2=?", 2+2, 4);
    }
}

Идея которая лежит в основе довольно интересная и работает это все не очень сложно. Если вкратце, то у вас базовый класс, в котором реализована шаблонная функция, которая предполагает параметризацию целым числом:


template <class Data>
class test_object : public Data
{
    /**
     * Default do-nothing test.
     */
    template <int n>
    void test()
    {
        called_method_was_a_dummy_test_ = true;
    }
}

Теперь когда вы пишете такой тест:


template<> template<>
void object::test<1>()
{
    ensure_equals("2+2=?", 2+2, 4);
}

Вы фактически создаете специализацию тестового метода для конкретного числа N=1 (именно для этого стоят template<>template<>). Вызвав test<N>() среда исполнения тестов может понять был ли это реальный тест или это была заглушка глядя на значение called_method_was_a_dummy_test_ после исполнения теста.


Далее, когда вы объявляете группу тестов:


tut::factory tf("basic test");

Вы, во-первых, совершаете перечисление всех test<N> до некоторой константы, зашитой в библиотеку, и, во-вторых, побочным эффектом добавляете в глобальный контейнер информацию о группе (имя группы и адреса всех тестовых функций).


В качестве условий проверки в tut используются исключения, так что функция tut::ensure_equals() просто бросит исключение если переданные ей два значения не будут равны, а среда запуска теста поймает исключение и засчитает тест как failed. Мне нравится такой подход, любому разработчику C++ становится сразу понятно, где можно использовать такие ассерты. Например, если мой тест создал вспомогательный поток, то там ассерты расставлять бесполезно, их никто не поймает. Кроме того, мне понятно, что мой тест должен иметь возможность освободить ресурсы в случае возникновения исключения, как будто это обычный exception-safe код.


В принципе библиотека tut-framework выглядит довольно неплохо, но в её реализации есть некоторые недостатки. Например, для моего случая я бы хотел, чтобы у теста был бы не только номер, но и другие атрибуты, в частности имя, а также "размер" теста (например интеграционный ли это тест или это unit тест). Это решаемо в рамках API tut, и даже что-то уже есть, а что-то можно реализовать, если добавить в API библиотеки метод, а в тело теста его вызов чтобы установить какие-нибудь его параметры:


template<> template<>
void object::test<1>()
{
   set_name("2+2"); // Set test name to be shown in test report
   ensure_equals("2+2=?", 2+2, 4);
}

Другая проблема в том, что среда запуска тестов tut ничего не знает о таком событии как начало теста. Среда выполняет object::test<N>() и она заранее не знает реализован ли тест для данного N, или это просто заглушка. Узнает она только когда тест закончится, проанализировав значение called_method_was_a_dummy_test_. Эта особенность не очень хорошо показывает себя в системах CI, которые умеют группировать вывод, который делала программа между началом и окончанием теста.


Однако на мой взгляд главная вещь которую можно улучшить ("фатальный недостаток") это наличие лишнего вспомогательного кода, требуемого для написания тестов. В tutorial tut-framework довольно много всего: предлагается сначала создать некий класс struct basic{}, а тесты описывать как методы объекта связанные с этим. В этом классе можно определить методы и данные, которые вы хотите использовать в группе тестов, а конструктор и деструктор обрамляют выполнение теста, создавая такую штуку как fixture из jUnit. На моей практике работы с tut этот объект почти всегда пустой, однако он тащит за собой какое-то количество строк кода.


Итак, заходим в вело-мастерскую и пробуем оформить идею в виде небольшой библиотеки.


Вот так выглядит минимальный файл теста в библиотеке "tested":


// Test group for std::vector (illustrative purposes)
#include "tested.h"
#include <vector>

template<> void tested::Case<CASE_COUNTER>(tested::IRuntime* runtime)
{
   runtime->StartCase("emptiness");
   std::vector<int> vec;
   tested::Is(vec.empty(), "Vector must be empty by default");
}

template<> void tested::Case<CASE_COUNTER>(tested::IRuntime* runtime)
{
   runtime->StartCase("AddElement");

   std::vector<int> vec;
   vec.push_back(1);
   tested::Is(vec.size() == 1);
   tested::Is(vec[0] == 1);
   tested::FailIf(vec.empty());
}

void LinkVectorTests()
{
   static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
}

Кроме отсутствия макросов бонусом идет отсутствие использование динамической памяти внутри библиотеки.


Определение тест-кейсов


Для регистрации тестов используется шаблонная магия начального уровня на том же принципе что и tut. Где-то в tested.h есть шаблонная функция такого вида:


template <int N> static void Case(IRuntime* runtime) { throw TheCaseIsAStub(); }

Тест кейсы, которые пишут пользователи библиотеки — это просто специализации этого метода. Функция объявлена статической, т.е. в каждом translation unit мы создаем специализации, которые не пересекаются по именам друг с другом при линковке.


Есть такое правило что вначале надо вызывать StartCase(), которому можно передать такие вещи как имя теста и возможно некоторые другие штуки, которые пока в разработке.


Когда тест вызывает runtime->StartTest() могут случится интересные вещи. Во-первых, если тесты сейчас в режиме запуска, то вы можете сообщить куда-то о том, что тест начал выполнение. Во-вторых, если сейчас идет режим сбора информации о доступных тестах StartTest() выбросит специального рода исключение которое будет означать что тест реальный, а не заглушка.


Регистрация


В какой-то момент нужно собрать адреса всех тест-кейсов и где-то их сложить. В tested это делается с помощью групп. Делает это конструктор класса tested::Group в виде побочного эффекта:


static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);

Конструктор создает группу с указанным именем и добавляет в нее все кейсы Case<N> которые найдет в текущем translation unit. Получается, что в одном translation unit у вас не может быть две группы. Это значит также что вы не можете одну группу разбить на несколько translation units.


Параметром шаблона идет сколько тест-кейсов искать в текущем translation unit для создаваемой группы.


Линковка


В приведенном примере создание объекта tested::Group() происходит внутри функции, которую мы должны позвать из нашего приложения чтобы зарегистрировать тесты:


void LinkStdVectorTests()
{
    static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
}

Функция не всегда требуется, иногда можно просто объявить объект класса tested::Group внутри файла. Однако мой опыт такой что линкер иногда "оптимизирует" файл целиком, если он собран внутри библиотеки, и никто из основного приложения не использует каких-либо символов этого cpp файла:


calc.lib    <- calc_test.lib(calc_test.cpp)
    ^               ^
    |               |
app.exe        run_test.exe  

Когда из исходников run_test.exe никак не связываются calc_test.cpp, то линкер просто убирает этот файл из рассмотрения целиком, вместе с созданием статического объекта, не смотря на то, что него есть нужные нам побочные эффекты.


Если какая цепочка приводит из run_test.exe, то статический объект появится в исполняемом файле. Причем неважно как именно это будет сделано, как в примере:


void LinkStdVectorTests()
{
    static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
}

или так:


static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
void LinkStdVectorTests()
{
}

Первый вариант на мой взгляд лучше тем что вызов конструктора выполняется после начала работы main(), и у приложения есть некоторый контроль над этим процессом.


Я думаю что эта установка костылей требуется для любой библиотеки юнит-тестирования, которая использует глобальные переменные и побочные эффекты конструктора для создании базы тестов. Однако наверное ее можно избежать если линковать тестовую библиотеку с ключом --whole-archive (аналог в MSVC появился только в Visual Studio 2015.3).


Макросы


Я обещал, что здесь не будет макросов, но он есть — CASE_COUNTER. Рабочий вариант что это используется __COUNTER__, макрос, который компилятор увеличивает на один каждый раз, когда он используется внутри translation unit.
Поддерживается GCC, CLANG, MSVC, но не стандартом. Если это расстраивает, то вот какие есть альтернативы:


  • использовать цифры 0, 1, 2
  • использовать стандартный __LINE__.
  • использовать constexpr магию 80 уровня. Можно поискать "constexpr counter" и попытаться найти компилятор, на котором это будет работать.

Проблема с __LINE__ в том, что использование больших чисел в параметрах шаблона создает большой размер исполняемого файла. Именно поэтому я ограничил тип шаблона signed char, получая 128 как максимальное количество тестов в группе.


Отказ от динамической памяти


Оказалось, что при регистрации тестов можно не использовать динамическую память, чем я и воспользовался. Возможно, в вашей среде нет динамической памяти или вы используете поиск утечек памяти в тест-кейсах, так что вмешательство среды исполнения тестов — это не то, что вам нужно. Google Test с этим борется, вот фрагмент оттуда:


// Use the RAII idiom to flag mem allocs that are intentionally never
// deallocated. The motivation is to silence the false positive mem leaks
// that are reported by the debug version of MS's CRT which can only detect
// if an alloc is missing a matching deallocation.
// Example:
//    MemoryIsNotDeallocated memory_is_not_deallocated;
//    critical_section_ = new CRITICAL_SECTION;
class MemoryIsNotDeallocated

А мы можем просто не создавать трудностей.


Как же мы тогда получаем список тестов? Это больше технические внутренности, которые проще посмотреть в исходном коде, но я все равно расскажу.


При создании группы ее класс получит указатель на функцию tested::CaseCollector<CASE_COUNTER>::collect, которая соберет все тесты translation unit в список. Вот как это устроенно:


// Make the anonymouse namespace to have instances be hidden to specific translation unit
  namespace {

  template <Ordinal_t N>
  struct CaseCollector
  {
     // Test runtime that collects the test case
     struct CollectorRuntime final : IRuntime
     {
        void StartCase(const char* caseName,
           const char* description = nullptr) final
        {
           // the trick is exit from test case function into the collector via throw
           throw CaseIsReal();
        }
     };

 // Finds the Case<N> function in current translation unit and adds into the static list. It uses the 
 // reverse order, so the case executed in order of appearance in C++ file.
 static CaseListEntry* collect(CaseListEntry* tail)
 {
    CaseListEntry* current = nullptr;

    CollectorRuntime collector;
    try
    {
       Case<N>(&collector);
    }
    catch (CaseIsStub)
    {
       current = tail;
    }
    catch (CaseIsReal)
    {
       s_caseListEntry.CaseProc = Case<N>;
       s_caseListEntry.Next = tail;
       s_caseListEntry.Ordinal = N;
       current = &s_caseListEntry;
    }

    return CaseCollector<N - 1>::collect(current);
 }

  private:
     static CaseListEntry s_caseListEntry;
  };

// This static storage will be instantiated in any cpp file
template <Ordinal_t N> CaseListEntry CaseCollector<N>::s_caseListEntry;    
}

Получается что в каждом translation unit создается много статических переменных вида CaseListEntry CaseCollector\::s_caseListEntry, которые являются элементами списка тестов, а метод collect() собирает эти элементы в односвязный список. Примерно таким же образом список формирует группы тестов, но без шаблонов и рекурсии.


Структура


Тестам нужна различная обвязка, вроде вывода в консоль красными буквами Failed, создание тест-репортов в формате понятном для CI или GUI в котором можно посмотреть список тестов и запустить выбранные — в общем много чего. У меня есть виденье как это можно сделать, которое отличается от того, что я видел раньше в библиотека тестирования. Претензия главным образом к библиотекам которые называют себя "header-only", при этом включая большой объем кода, который по сути совсем не для заголовочных файлов.


Подход, который я предполагаю в том, что мы разделяем библиотеку на front-end — эта сама tested.h и back-end — библиотеки. Для написание тестов нужен только tested.h, который сейчас C++17 (из-за здоровского std::string_view) но предполагается что будет C++98. Tested.h осуществляет фактически регистрацию и поиск тестов, минимально удобный вариант запуска, а также возможность экспорта тестов (группы, адреса функций тест-кейсов). Back-end библиотеки, которых еще не существует, могут делать все что нужно, в плане вывода результатов и запуска, использовав функционал экспорта. Таким же образом можно приспособить запуск под нужды своего проекта.


Итог


Библиотеке tested (код на github) предстоит еще некоторая стабилизация. В ближайших планах добавить возможность выполнения асинхронных тестов (нужно для интеграционный тестов в WebAssembly) и указания размера тестов. На мой взгляд библиотека еще не вполне готова к production применению, но я потратил уже неожиданно много времени и наступил такой этап чтобы остановится, перевести дух и спросить обратной связи от сообщества. Интересно ли вам было бы воспользоваться такого рода библиотекой? Может быть в арсенале С++ есть какие-нибудь еще идеи как было бы можно создать библиотеку без макросов? Интересна ли вообще такая постановка задачи?

Поделиться публикацией

Комментарии 27

    +11
    И всё-таки, почему макросы, используемые для уменьшения многословности — это плохо?
      –7
      По той же причине, почему в С++ придумали шаблоны — потому что, цитирую: «макросы — это неотлаживаемое наследие С, от которого, как и от оператора goto надо избавиться любой ценой»
        +13
        И столь же зря про «любой ценой».
          +3
          неотлаживаемое

          Попробуйте прошагать в отладчике что-то, использующее boost. Да фиг с ним, с бустом, просто с банальным STL.
            +3

            Попробовал. И что?

            0

            А откуда цитата? Мой гугл такой фразы не знает.

              +2
              У вас плохой Гугл. Хороший через 15 минут после появления этой фразы её уже должен знать. И сюда вести…
            +3
            Не то чтобы я принципиально против макросов, но ведь можно поисследовать подход как без них можно было бы обойтись. В C++ например заменили "#define max(x, y) x > y? x: y" на другой вариант с inline функциями и есть вполне понятные причины почему так было сделано.

            В случае с макросами в библиотеке тестирования мне не нравится что не очень понятно что происходит на уровне языка когда я пишу тест. Когда разрабатываются unit-тесты как по учебнику, это понимание может быть не требоваться, но когда это какой-нибудь тяжелый интеграционный тест мне надо выяснять как работают все эти дополнительные волшебные коробочки завернутые в макросы и как они поведут себя в моем сложном тесте. В этом смысле голый С++ ближе для понимания, потому что его все уже знают и не требуется учить некий новый DSL на макросах. Еще одна претензия к макросам что исходники библиотеки написанной на макросах довольно тяжело читать.

            В итоге, мне показалось интересным покопать вопрос насколько далеко можно уменьшить boilerplate код если выставить ограничение «без макросов».
              –1
              Под капотом там, очевидно, регистрация.
              Проводя аналогию: для того, чтобы использовать std::vector, необходимо знать что там у него под капотом?
              А у Вас получается, что за деревьями леса не видно: наружу торчит много низкоуровневых деталей реализации теста, плюс ограничение в духе «640 килобайт хватит всем!»
                0
                А что за ограничения вам показались напрягающими?

                Про vector: мне обязательно надо знать что под капотом std::vector, по крайней мере много чего из внутреннего устройства (есть ли там блокировки, где и когда выделяется память, как он растет когда мы туда добавляем элементы — как минимум).
                  0
                  А что за ограничения вам показались напрягающими?

                  Ну например отключенные в мелких embedded-системах исключения.

                  Про vector: мне обязательно надо знать что под капотом std::vector
                  vs чуть выше
                  В случае с макросами в библиотеке тестирования мне не нравится что не очень понятно что происходит на уровне языка когда я пишу тест.

                  Вы бы определились что ли.

                  Библиотеки предоставляют абстракции для сокрытия сложности. В C++ есть много способов это сделать (и добавить тоже), в том числе пришедшие в наследство из C макросы.
            0
            использовать constexpr магию 80 уровня. Можно поискать "constexpr counter" и попытаться найти компилятор, на котором это будет работать.

            Уже бессмысленно, магию исправили где-то между C++14 и 17.

              +1
              Получается что окончательно исправили? Я видел что были статьи и доклады на конференциях, а потом примечания что «что-то перестало работать».
              +1
              Может быть в арсенале С++ есть какие-нибудь еще идеи как было бы можно создать библиотеку без макросов?
              Например, у cxxtest такая идея: тесты пишутся обычными методами в классах, которые определены в h-файлах, а main.cpp, запускающий эти тесты по очереди и подсчитывающий статистику успехов, генерируется питон-скриптом, который довольно легко запускать перед компиляцией через makefile, или задачей «before build» в проекте Visual Studio.
                0
                Довольно интересный подход с кодогенерацией, о таком даже не думал.
                  0
                  В питоне и C# эта проблема хорошо решается рефлексией. Можно проанализировать код тестов и выделить функции, которые необходимо запустить как тесты. Если в языке нет рефлексии, логично этот шаг сделать внеязыковыми средствами. Вот ещё свежий примерчик, если вы не видели: Тесты на Си без SMS и регистрации
                    0
                    Рефлексия конечно помогает, я даже хотел ввернуть это в статью, что мол у других хоть рефлексия есть. Спасибо за ссылку на статью, мой RSS еще не видимо не прогрузил ее. Пример с cutter — вообще бомба, правда со своими компромиссами.
                +2
                Интересна ли вообще такая постановка задачи?

                Без подробного сравнения с google test/boost test выглядит не очень интересно.
                Потому что "на вскидку", без подробного анализа сложно понять насколько это нужные изменения, а вот насколько все ухудшилось видно сразу.


                Поясню:


                без макросов

                Ошибки которые потенциально могут внести макросы, это "TestCase" не зарегистрируется и таким образом не запуститься или "assert" реализованный с помощью макросов не заметит ошибки. Честно говоря за > 10 лет опыта я такого ни разу не видел, но это конечно не аргумент, аргументом был бы анализ используемых макросов с описанием как можно было ошибиться в их использовании.


                и динамической памяти

                А какое количество TestCase нужно написать чтобы это стало заметно,
                чтобы скажем запуск тестов замедлился бы время большее 1 секунды на современной машине?
                Миллион, два? Существует ли в мире хотя бы один проект с таким количеством "TestCase".


                Плюс на мой взгляд по приоритету важнее:


                1. Сколько нужно кода чтобы описать testcase и написать assert. В вашем варианте я как понимаю нужно в два раза больше строчек кода на каждый testcase по сравнению со "стандартными"?
                2. Сколько занимает запуск измененного testcase? То есть время компиляции+линковки+собственно время выполнения программы с юнит-тестами.
                  На первый взгляд время выполнения должно измениться в пределах погрешности,
                  а вот компиляция+линковка?
                3. Возможно фильтрации testcase, запуск всех тестов название которых удовлетворяет регулярному выражению.

                То есть если 1-3 у "test framework" одинаковы или лучше, то можно рассматривать "макросы" и "динамическую память", хотя если "лучше", то я бы и не стал смотреть на "макросы" и "динамическую память", а сразу бы стал использовать ваш "test framework".

                  +1
                  На самом деле местами посылы правильные.
                  Есть такая категория разработок, как Embedded, где динамическая память часто бывает либо обрезана, либо имеет ограничения. Но там, как правило, и исключения порезаны в ноль, поэтому в полной мере указанные подходы использовать нельзя. А исключения как правило обходятся куда дороже чем динамическое выделение памяти.
                    0

                    Ну встраиваемая разработка, где еще и памяти мало, это не то на что стоит ориентироваться автору, а то попытка усидеть на двух стульях может печально закончится.


                    Из личного опыта. Для запуска юнит тестов (для С кода правда, но в данном случае это не принципиально) разработали собственный "framework". Каждый "testcase" компилировался в отдельный бинарник, линковщик выкидывал не нужное и поэтому (а также благодаря хорошей модульности) бинарники были достаточно маленькие чтобы не прошивать их на флешку, а загружать прямо в SRAM. А потом специальный скрипт по JTAG грузил сначала инициализатор периферии, а потом последовательно каждый testcase.
                    Очень сомнительно, что "test framework" общего назначения даже без использования кучи нам бы подошел.


                    Хотя это конечно субъективно, но я бы не стал заморачиваться пытаясь решить такие случаи. Нужно ведь и интерфейс как надергать "test case" в отдельные бинарники и и возможно в некоторых случаях вызывать в assert не std::abort/std::terminate, а бесконечный цикл с миганием лампочками и прочие странные и специфичные вещи.

                      0
                      В проекте на котором я работаю исключения отключены в основном коде, но включены в тестах (из-за tut). С исключениями бывают проблемы даже не столько в embedded, сколько в неожиданных платформах вроде pnacl (где они были реализованы не сразу) или Emscripten (у меня с ними проблем не было, но судя по документации они там отключены по умолчанию в -o1 потому что реализация довольно дорогая).

                      Про говорить про «киллер-фичу» с динамической памятью, то я смотрю на это так что динамическая память зашитая внутрь библиотеки — это как дополнительная зависимость. Если отсутствие такой дополнительной зависимости обходится недорого, то от нее можно отказаться. В этом случае библиотека может подойти как тем кому это не принципиально, так и тем кому это важно.
                      0
                      Миллион, два? Существует ли в мире хотя бы один проект с таким количеством «TestCase».
                      Миллион не нужен. В DEQP порядка трёхсот тысяч тестов и старт занимает заметное время на слабых телефонах. Впрочем там не только из-за харнеса задержки при создании, так что не факт, что предлагаемый подход «спасёт отца русской демократии».
                        –1
                        Спасибо за замечания, мне это очень интересно! Библиотека в таком MVP состоянии, так что такого рода замечания наводят меня на мысли где бы ее можно было бы улучшить прежде всего.

                        Сколько нужно кода чтобы описать testcase и написать assert.

                        Assert примерно одинаково в обоих случаях, а вот определения тесто в моем случае явно чуть побольше. Если привести примеры из статьи:

                        TEST(MyString, DefaultConstructor) {
                        }


                        versus:

                        template<> void tested::Case<CASE_COUNTER>(tested::IRuntime* runtime)
                        {
                           runtime->StartCase("emptiness");
                        }
                        // ... and one group per translation unit
                        static tested::Group<CASE_COUNTER> x("std.vector", __FILE__);
                        


                        То есть увеличение есть и может быть можно подумать как его уменьшить (а обернуть в макрос всегда успеем). Но мне кажется что если смотреть с позиции гранулярности тестов то дополнительный код не такой значительный. По моему опыту когда тесты особенно интеграционные вполне выходят за 10+ строк, так что выигрыш по строкам на моих тестах будет не слишком значительным.

                        2. Сколько занимает запуск измененного testcase?

                        Я не встраивал эту библиотеку в большой проект, но если честно то я не ожидаю какого-то оверхеда у tested. Изменение test-case это пересборка *.cpp файла + линковка, вроде нет ничего что можно было бы улучшить. Запуск теста — это то где можно что-то по-улучшать если это будет тонким местом.

                        Момент в производительности который я тестировал — это какой размер исполняемого файла можем получить если укажем глубокий уровень рекурсии для перебора шаблонной функции (типа Group<1024*1024> — я не дождался окончания компиляции). То есть если у вас в translation unit очень много тестов (скажем 1000), то это может увеличивать время компиляции, размер бинарника и поисков по списку. Чтобы на такого рода ограничения не попадать, я пока ограничил количество тестов в группе положительным диапазоном типа signed char (0-127).

                        3. Возможно фильтрации testcase, запуск всех тестов название которых удовлетворяет регулярному выражению.


                        Тут неплохо бы фидбэк насколько это нужно. То что я реализовал сейчас, это возможность запуска определенного теста и всех тестов в определенной группе. Например у нас группа «std.container.vector» и тест «construction». Получается что у теста можно сделать штуку под названием адрес, «std.container.vector:construction». И далее запускать регулярные выражения по таком адресу «std.container.*» или «std.container.vector:*». Но только не вполне понятно какой именно кейс для запуска именно по регулярным выражениям?
                        +4

                        Не плохой фреймворк, видимо, если спрятать boilerplate code в макросы, может получиться почти так же удобно, как в gtest.

                          0
                          Хе-хе, я думал об этом. Но тут хотя бы есть выбор, можешь использовать макросы, можешь не использовать. В gtest попытка писать тесты развернув все макросы будет существенно сложнее.
                            0

                            Это да, но волков бояться — в лес не ходить. Когда будет распространено constexpr пошире, да побольше возможностей кодогенерации хотя бы с тем же уровнем лаконичности на этапе использования, который сейчас даёт препроцессор, тогда можно будет и думать о том, что бы отказываться от возможности сэкономить огромное количество человекочасов и человеконервов. Тесты должны писаться быстро, что бы их было не жалко выкидывать.


                            Второй момент — много мусора в заголовках кейсов, очень не выразительно. А при чтении это первое куда нужно смотреть. И тут даже сложно понять, чей подход лучше gtest или catch, но точно не то, что продемонстрировано в посте.


                            Возможно я не прав по поводу фреймворка и просто не удачные примеры в статье. Тогда, было бы здорово увидеть альтернативу для такого набора сценариев, не вдаваясь в тела самих кейсов:


                            TEST_F(CorruptedStorage, try_write_block){//...}
                            TEST_F(CorruptedStorage, try_read_block){//...}

                            На мой взгляд, там и так много буков, на предлагаемый функционал, например в pytest это выглядело бы примерно так:


                            def test_try_write_block(corrupted_storage):
                                pass
                            
                            def test_try_read_block(corrupted_storage):
                                pass
                              0
                              Согласен, выразительность определения теста страдает. Это интересная наводка, спасибо. Не знаю можно ли ее будет решить как-то не в макросах, каких-то сходу идей нет.

                              Некоторые изменения в API которые думаются на ближайшее время, это возможность указания «размера» теста, которое может быть выглядеть как параметр StartCase():
                              template<> void tested::Case<CASE_COUNTER>(tested::IRuntime* runtime)
                              {
                                 runtime->StartCase("try_write_block", INTEGRATION_TEST);
                              }
                              


                              А также указание на то что тест асинхронный:
                              template<> void tested::Case<CASE_COUNTER>(tested::IRuntime* runtime)
                              {
                                 tested::callback *cb = runtime->StartAsyncCase("try_write_block");
                              }
                              


                              Т.е. некоторая избыточность вроде метода StartCase() она все равно будет нужна для других целей.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое