Тестирование с помощью Libtap

Тестирование с помощью Libtap

Наличие набора качественных тестов – ключевая часть этой стратегии, позволяющей вам изменять внутреннею архитектуру приложения, оставаясь уверенными, что API приложения не нарушен. Эта статья описывает различные способы использования Perl для тестирования вашего кода на C.

Тестирование с помощью Libtap

Stig Brautaset, перевод Владимир Куксенок

Libtap – это библиотека для тестирования кода на языке C. Она реализует Test Anything Protocol, использующийся в среде тестирования Perl.

Сегодня проектирование, завтра программирование

Одна из идей экстремального программирования звучит как “сегодня проектирование, завтра программирование”. Вместо того чтобы детально прорабатывать архитектуру приложения, вы должны писать код, легко изменяемый при необходимости.

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

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

Введение в Test Anything Protocol

Дистрибутивы Perl обычно поставляются со средой тестирования, написанной с использованием Test::Simple, Test::More или старого (который не рекомендуется использовать) модуля Test. Эти модули в соответствии с Test Anything Protocol (TAP) содержат функции для вывода текстового сообщения в зависимости от успешного или неуспешного выполнения теста. Вывод тестовой TAP программы выглядит примерно следующим образом:
1..4
ok 1 - the WHAM is overheating
ok 2 - the overheating is detected
not ok 3 - the WHAM is cooled
not ok 4 - Eddie is saved by the skin of his teeth

Строка “1..4” означает, что ожидается выполнение четырех тестов. Это может помочь вам определить ситуацию, когда тестовый скрипт завершит работу до выполнения всех тестов. Остальные строки содержат флаг выполнения теста, “ok” или “not ok”, номер теста, а также имя теста или короткое описание.

Perl модули обычно выполняют тестирование, запуская соответствующую программу или исполняя make test или ./Build test (в зависимости от того, что вы используете, ExtUtils::MakeMaker или Module::Build). Все три способа используют модуль Test::Harness для анализа вывода TAP тестов. Если все тесты завершатся неудачей, вы сможете запустить отдельные тесты напрямую и вручную анализировать результаты.

Если в Test::Harness передать список программ тестирования, каждая из них будет запущена, а результаты их работы будут просуммированы. Тесты могут запускаться в кратком и подробном режиме. В кратком режиме выводится только имя тестового скрипта (или скриптов) и суммарный результат. В подробном режиме выводится имя каждого теста.

Кроме Perl, библиотеки для генерации TAP вывода доступны для многих языков, включая C, Javascript и PHP (см. раздел Ссылки).

Предположим, вы хотите написать тесты для модуля Foo, в котором реализованы функции mul(), mul_str() и answer(). Первые две функции выполняют умножение чисел и строк (т.е. чисел в строковом представлении), тогда как третья отвечает на вопросы о жизни, вселенной и всем остальном. Вот очень простой скрипт для тестирования этого модуля:

use Test::More tests => 3; 
use Foo;

ok(mul(2,3) == 6, '2 x 3 == 6');
is(mul_str('two', 'three'), 'six', 'expected: six');
ok(answer() == 42, 'got the answer to everything');

“tests => 3” говорить Test::More сколько тестов предполагается выполнить (назовем это планированием). Данная запись позволяет обнаруживать факты выхода из скрипта тестирования без завершения выполнения всех тестов. Можно писать скрипты без планирования, однако многие считают это плохой привычкой.

Тестирование кода на языке C

Libtap - это реализация TAP на языке C, повторяющая функционал модуля Test::More.

Libtap - это удобный способ заставить ваши программы говорить на языке протокола TAP. Эта библиотека позволяет вам описывать, какое количество тестов вы хотите выполнить, какие тесты необходимо пропустить (например, выполняющиеся только для определенных ОС) и помечать тесты для еще нереализованных возможностей как TODO. Библиотека также содержит функцию exit_status(), через код возврата программы передающую факт наличия или отсутствия неудач при тестировании.

Как написать тест для модуля Foo на языке C, используя Libtap? Строка “#include ” является аналогом “use Foo;” в версии для Perl. Однако, так как это C, вам также нужно прилинковать библиотеку libfoo (реализующую функции, описанные в foo.h).

В данном случае, я покажу полный исходный код программы-теста, включая все строки #include. Далее, я буду приводить только короткие фрагменты программ. Обратите внимание на число, переданное в функцию plan_tests() и на реальное количество тестов.

#include 
#include
#include

int main(void) {
plan_tests(3);
ok1(mul(2, 3) == 6);
ok(!strcmp(mul_str("two", "three"), "six"), "expected: 6");
ok(answer() == 42, "got the answer to everything");
return exit_status();
}

Функция exit_status() возвращает 0, если было выполнено правильное число тестов, и все они завершились успешно. В противном случае возвращается ненулевое значение. Версия тестовой среды для Perl сама устанавливает код выхода (exit status), позволяя не делать этого вручную.

Одно заметное различие между версиями для Perl и C - это макрос ok1(), являющийся оберткой над функцией ok(). Вместо того, что вызывать ok() с условием в качестве первого параметра и именем теста в качестве второго параметра, этот макрос конвертирует передаваемый аргумент в строку и использует его как имя теста. Это может быть очень удобно для простых тестов.

Оба приведенных выше теста на Perl и C при выполнении выводят следующее:

1..3 
not ok 1 - mul(2, 3) == 6
# Failed test (basic.c:main() at line 12)
ok 2 - expected: 6
ok 3 - got the answer to everything

Строка, начинающаяся с “#” это вспомогательное сообщение. Libtap печатает его, чтобы помочь вам определить, какой тест завершился неудачей. В данном случае, это сообщение содержит номер строки в файле, в котором находился неудавшийся тест.

Пропуск тестов

Иногда необходимо пропустить некоторые тесты. Например, вы тестируете функцию, которую может выполнять только root, или какой-либо код, привязанный к определенной платформе. Используя Test::More, вы можете создать специальный блок кода, помеченный специальной меткой SKIP:

SKIP: { 
skip 2, "because only root can foo()"
unless is_root();
ok(foo(0), "root can foo(0)");
ok(foo(1), "root can foo(1)");
}

С помощью libtap вы не можете написать также, однако функция skip() имеет то же предназначение. Эта функция в качестве аргумента принимает номер теста для пропуска и строку, описывающую причину пропуска теста. Ниже приведена С версия предыдущего примера на Perl.

if (is_root()) { 
ok(foo(0), "root can foo(0)");
ok(foo(1), "root can foo(1)");
}
else {
skip(2, "because only root can foo()");
}

Обратите внимание, если количество тестов в ветке is_root() условного перехода изменится, вы должны изменить первый аргумент, передаваемый в функцию skip() ветки условного перехода !is_root(). Для простых случаев, подобных показанному, это очень просто осуществить, однако при больших объемах кода могут возникнуть трудности.

Libtap также предоставляет макросы skip_start() и skip_end, обеспечивающие более Perl-подобный способ пропуска тестов. Если первый аргумент, переданный в skip_start() равен true, libtap пропускает все тесты между ним и skip_end. Т.е. код скомпилируется, но выполняться не будет. Вы должны убедиться, что второй аргумент, переданный в skip_start(), равен правильному числу тестов между ним и skip_end. Плюсом является то, что не нужно заботиться о двух различных ветвях условного перехода, как в предыдущем примере.

skip_start(is_root(), 2, "because only root can foo()"); 
ok(foo(0), "root can foo(0)");
ok(foo(1), "root can foo(1)");
skip_end; /* it's a macro: no parentheses */

Независимо от способа, которым вы воспользуетесь, при запуске с правами root вывод будет выглядеть примерно следующим образом:

1..2 
not ok 1 - root can foo(0)
# Failed test (skip.c:main() at line 12)
ok 2 - root can foo(1)
# Looks like you failed 1 tests of 2.

И то же самое, запущенное с правами обычного пользователя:

1..2 
ok 1 # skip only root can foo()
ok 2 # skip only root can foo()

TODO тесты

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

Для написания TODO тестов на Perl нужно установить значение переменной $TODO в true. Test::More использует это значение как индикатор того, что описанные тесты созданы для еще не реализованных возможностей.

ok(run(), "yay, it claims to start running!"); 
ok(is_running(), "it is running");

{
local $TODO = "not sussed this part yet...";
ok(stop(), "it appears to stop");
ok(!is_running(), "it is not running");
}

На C это выглядит примерно так же, но вместо блока TODO вы должны использовать соответствующие функции:

ok(run(), "yay, it claims to start running!"); 
ok(is_running(), "it is running");

todo_start("not sussed this part yet...");
ok(stop(), "it appears to stop");
ok(!is_running(), "it is not running");
todo_end();

Предположим, что программа, которую вы тестируете, выдает сообщение о том, что выполнение чего-либо остановлено, а на самом деле остановки не было. Вывод должен быть примерно следующим:

1..4 
ok 1 - yay, it claims to start running!
ok 2 - it is running
ok 3 - it appears to stop # TODO not done yet...
not ok 4 - it is not running # TODO not done yet...
# Failed (TODO) test (todo.c:main() at line 17)

“Правильное” планирование

Что произойдет, если вы укажите неверное число тестов? Попробуйте сделать это:

plan_test(3);
ok(1, "true");
ok(!0, "!false == true");
return exit_status();

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

% ./plan-too-many && echo success || echo failure
1..3
ok 1 - 1 is true
ok 2 - !0 is true
# Looks like you planned 3 tests but only ran 2.
failure

Libtap содержит встроенный механизм, позволяющий определить, что вы запланировали слишком много тестов. В данном случае этот факт был определен верно, однако вы можете получить такое же сообщение, если серьезные ошибки приведут к завершению работы программы, прежде чем все тесты выполнятся. В дополнение было выведено “failure”, что означает, что программа тестирования вернула ненулевое значение, являющееся индикатором ошибки. Это произошло благодаря функции exit_status(), позволяющей для определения результата тестирования просто запустить тест и игнорировать вывод (хотя в данном случае это не было бы фактом неудачи одного из тестов).

Задачи тестирования

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

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

Вместо этого я разделил тестирование на два этапа. Вначале я написал простую программу на C для перемещения по сетке и вывода состояния на каждом шаге. Этот код выглядел примерно следующим образом:

state = init_othello_state(); 
do {
print_othello_state(state);
putchar('\n');
} while (state = ai_move(state));

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

local $/ = ''; 
my @states = qx( ./reversi )
or die 'running reversi failed';

is shift @states, $_ while ;

__DATA__
......
......
..ox..
..xo..
......
...... - x to move

best branch: 6 (ply 3 search; 48 states visited)
......
......
..ox..
..xx..
...x..
...... - o to move

best branch: 1 (ply 3 search; 45 states visited)
......
......
..ox..
..ox..
..ox..
...... - x to move

[и т.д.]

Заметьте, в последнем тесте вообще не использовался libtap. Вместо этого, использовался модуль на C и модуль на Perl. Используя Inline::C, вы можете улучшить тест, поместив код на C и Perl в один файл. Обратите внимание, использование двух отдельных модулей предпочтительней, поскольку, если Perl будет не доступен, модуль на C все же можно будет использовать, хотя и придется вручную сравнивать его результаты с заранее известными верными состояниями.

Доступность

Test::More и Test::Harness являются частью ядра в последних версиях Perl. Библиотека Libtap, к сожалению, обычно не установлена по умолчанию. Однако Libtap имеет либеральную лицензию и состоит всего из двух исходных файлов, поэтому, если вы хотите быть уверены, что ваши пользователи смогут запустить ваши наборы тестов, вы можете включать Libtap в свои программы.

Заключение

Эта статья описывает различные способы использования Perl для тестирования вашего кода на C. Вначале, в ней кратко описывается Test Anything Protocol, являющийся основой тестовой среды Perl. Затем показано как Libtap может помочь вам генерировать совместимый с TAP вывод об ошибках из программ на C. В завершение статьи мы используем Perl для тестирования программ, сравнивая результаты работы с заранее известными верными результатами.

Надеюсь, эта статья поможет вам улучшить свои собственные наборы тестов.

Ссылки

Домашний Wi-Fi – ваша крепость или картонный домик?

Узнайте, как построить неприступную стену