Хабрахабр

Тесты на Си без SMS и регистрации

Автору (почти) удалось избежать использования макросов для регистрации тестов, однако вместо них в коде появились «волшебные» шаблоны, которые лично мне кажутся, простите, невообразимо уродскими. скришот CutterНедавно zerocost написал интересную статью «Тесты на C++ без макросов и динамической памяти», в которой рассматривается минималистический фреймворк для тестирования Си++ кода. Я сразу не смог вспомнить где, но я точно видел код тестов, который не содержит ни единого лишнего символа для их регистрации: После прочтения статьи у меня оставалось смутное чувство неудовлетворённости, так как я знал, что можно сделать лучше.

void test_object_addition()
{ ensure_equals("2 + 2 = ?", 2 + 2, 4);
}

Наконец-то я вспомнил, что этот фреймворк называется Cutter и он использует по-своему гениальный способ идентификации тестовых функций.

(КДПВ взята с сайта Cutter под CC BY-SA.)

В чём же трюк?

Функции-тесты извлекаются из экспортируемых символов библиотеки и идентифицируются по именам. Тестовый код собирается в отдельную разделяемую библиотеку. Sapienti sat. Тесты исполняет специальная внешняя утилита.

$ cat test_addition.c
#include <cutter.h> void test_addition()
{ cut_assert_equal_int(2 + 2, 5);
}

$ cc -shared -o test_addition.so \ -I/usr/include/cutter -lcutter \ test_addition.c

$ cutter .
F
=========================================================================
Failure: test_addition
<2 + 2 == 5>
expected: <4> actual: <5>
test_addition.c:5: void test_addition(): cut_assert_equal_int(2 + 2, 5, )
========================================================================= Finished in 0.000943 seconds (total: 0.000615 seconds) 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s),
0 omission(s), 0 notification(s)
0% passed

Можно смело проматывать всё, что связано с Autotools, и смотреть только на код. Вот пример из документации Cutter. Фреймворк немного странный, да, как и всё японское.

У меня также нет полноценного (и даже хотя бы чернового) кода, так как лично мне он не очень-то и нужен (в Rust всё есть из коробки). Я не буду слишком уж подробно разбирать особенности реализации. Однако, для заинтересовавшихся людей это может быть хорошим упражнением.

Детали и возможности реализации

Рассмотрим некоторые задачи, которые потребуется решить при написании фреймворка для тестирования с помощью подхода Cutter.

Получение экспортируемых функций

Стандарт Си++, естественно, не описывает разделяемые библиотеки вовсе. Для начала, до тестовых функций необходимо как-то добраться. Как известно, POSIX-системы предоставляют функции dlopen(), dlsym(), dlclose(), с помощью которых можно получить адрес функции, зная имя её символа, и… в общем-то всё. Windows с недавних пор обзавелась Linux-подсистемой, что позвляет все три главные операционные системы свести к POSIX. Список функций, содержащихся в загруженной библиотеке, POSIX уже не раскрывает.

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

В качестве начального приближения можно просто вызывать утилиту nm:

$ cat test.cpp
void test_object_addition()
{
}

$ clang -shared test.cpp

$ nm -gj ./a.out
__Z20test_object_additionv
dyld_stub_binder

разбирать её вывод и пользоваться dlsym().

На самом деле nm и компания как раз ими и пользуются. Для более глубокой интроспекции пригодятся библиотеки вроде libelf, libMachO, pe-parse, позволяющие программно разбирать исполнимые файлы и библиотеки интересующих вас платформ.

Фильтрация тестовых функций

Как вы могли заметить, в библиотеках содержатся какие-то странные символы:

__Z20test_object_additionv
dyld_stub_binder

И что это за левая dyld_stub_binder? Вот что это за __Z20test_object_additionv, когда мы называли функцию просто test_object_addition?

Особенность компиляции Си++, ничего не поделаешь, живите с этим. «Лишние» символы __Z20... — это так называемое декорирование имён (name mangling). Для того, чтобы показывать их человеку в нормальном виде, можно воспользоваться библиотеками вроде libdemangle. Именно так называются функции с точки зрения системы (и dlsym()). Конечно же нужная библиотека зависит от используемого вами компилятора, но формат декорирования обычно одинаков в рамках платформы.

Какие-то функции вызывать при запуске тестов не надо, так как там рыбы нет. Что касается странных функций вроде dyld_stub_binder, то это тоже особенности платформы, которые придётся учитывать.

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

Передача контекста исполняемого теста

Соответственно, внутренние тестовые функции могут этим пользоваться. Объектные файлы с тестами собираются в разделяемую библиотеку, исполнение кода которой полностью контролируется внешней утилитой-драйвером — cutter для Cutter.

За управление и передачу контекста отвечает драйвер. Например, контекст исполняемого теста (IRuntime в исходной статье) можно спокойно передавать через глобальную (thread-local) переменную.

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

void test_vector_add_element()
{ testing::description("vector size grows after push_back()");
}

Безопасность использованя глобального контекста гарантируется фреймворком и не является ответственностью писателя тестов. Функция description() получает доступ к условному IRuntime через глобальную переменную и таким образом может передать фреймворку комментарий для человека.

При таком подходе в коде будет меньше шума с передачей контекста в утверждения сравнения и внутренние тестовые функции, которые может потребоваться вызывать из основной.

Конструкторы и деструкторы

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

В библиотеке Cutter для этого используются следующие функции:

  • cut_setup() — перед каждым отдельным тестом
  • cut_teardown() — после каждого отдельного теста
  • cut_startup() — перед запуском всех тестов
  • cut_shutdown() — после завершения всех тестов

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

Для Си++ возможно придумать более идиоматичный интерфейс:

  • более объектно-ориентированный и типобезопасный
  • с лучшей поддержкой концепции RAII
  • использующий лямбды для отложенного исполнения
  • задействующий контекст исполнения тестов

Но мне пока опять размышлять над этим всем в деталях сейчас.

Самодостаточные исполнимые файлы с тестами

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

Прочее

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

  • гибкие и расширяемые тестовые утверждения
  • построение и получение тестовых данных из файлов
  • исследование стектрейсов, обработка исключений и падений
  • настраиваемые «уровни поломки» тестов
  • запуск тестов в нескольких процессах

UX — гораздо более глубокая тема. Стоит оглядываться на существущие фреймворки при написании своего велосипеда.

Заключение

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

д. Особенности сборки и запуска тестов можно спрятать в переиспользуемые модули для систем сборки вроде Makefile, CMake, и т. Вопросами отдельной сборки тестов всё равно придётся так или иначе задаваться.

К сожалению, в таком случае без дополнительных подсказок уже не разобраться, какие функции запускать надо, а какие нет. Из недостатков такого подхода можно отметить сложность размещения тестов в том же файле (той же единице трансляции), что и основной код. К счастью, в Си++ обычно и так принято разносить тесты и реализацию в разные файлы.

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

void test_object_addition()
{ ensure_equals(2 + 2, 5);
}

но при этом сохраняя ту же информативность выдачи в случае ошибок:

Failure: test_object_addition
<ensure_equals(2 + 2, 5)>
expected: <5> actual: <4>
test.c:5: test_object_addition()

Ожидаемое и фактическое значение сравниваемых выражений известны функции ensure_equals(). Имя тестируемой функции, имя файла и номер строки начала функции в теории можно извлечь из отладочной информации, содержащейся в собираемой библиотеке. Макрос же позволяет «восстановить» исходное написание тестового утверждения, из которого более понятно, почему ожидается именно значение 4.

Заканчиваются ли на этом преимущества макросов для тестового кода? Впрочем, это на любителя. Гораздо более интересный вопрос: возможно ли как-то сделать мок-фреймворк для Си++ без макросов? Я пока особо не думал над этим моментом, который может оказаться хорошим полем для дальнейших извращений исследований.

Внимательный читатель также заметил, что в реализации действительно отсутствуют SMS и асбест, что является несомненным плюсом для экологии и экономики Земли.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»