Pull to refresh

Comments 27

И всё-таки, почему макросы, используемые для уменьшения многословности — это плохо?
UFO just landed and posted this here
И столь же зря про «любой ценой».
UFO just landed and posted this here

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

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

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

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

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

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

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

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

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

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


Поясню:


без макросов

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


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

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


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


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

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

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

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


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


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

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

Про говорить про «киллер-фичу» с динамической памятью, то я смотрю на это так что динамическая память зашитая внутрь библиотеки — это как дополнительная зависимость. Если отсутствие такой дополнительной зависимости обходится недорого, то от нее можно отказаться. В этом случае библиотека может подойти как тем кому это не принципиально, так и тем кому это важно.
Миллион, два? Существует ли в мире хотя бы один проект с таким количеством «TestCase».
Миллион не нужен. В DEQP порядка трёхсот тысяч тестов и старт занимает заметное время на слабых телефонах. Впрочем там не только из-за харнеса задержки при создании, так что не факт, что предлагаемый подход «спасёт отца русской демократии».
Спасибо за замечания, мне это очень интересно! Библиотека в таком 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:*». Но только не вполне понятно какой именно кейс для запуска именно по регулярным выражениям?

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

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

Это да, но волков бояться — в лес не ходить. Когда будет распространено 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
Согласен, выразительность определения теста страдает. Это интересная наводка, спасибо. Не знаю можно ли ее будет решить как-то не в макросах, каких-то сходу идей нет.

Некоторые изменения в 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() она все равно будет нужна для других целей.
Sign up to leave a comment.

Articles