суббота, 29 августа 2009 г.

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

Я вообще не отношу себя к адептам Test Driven Development, но считаю тесты довольно важной и удобной штукой. Правда дальше самопальных тестов к отдельным функциям и классам, выполняемых по #ifdef-у или в отдельном консольном проект дело не шло. Но ведь надо пользоваться чем-нибудь общепринятым, да и вообще уделять тестам больше внимания. Подумав так, решил встроить-таки хотя бы несколько тестов в свой дипломный проект.
В прошлый раз, когда я пытался приучить себя к тестам, меня отпугнула замороченость CppUnit (клона jUnit для C++), поэтому в этот раз я начал с обзора других средств для тестирования кода на C++.
Вообще я немного поковырял googletest, UnitTest++, и даже Boost Test Library, но речь сейчас не о них.
В конце концов, я искал простое и функциональное решение, безо всяких дурацких регистраций тестов и прочей ненужной синтаксической ерунды.
В конце концов я выбрал CxxTest.

CxxTest


Основные преимущества:
  • Не требует RTTI и шаблонов.
  • Не требует механизма исключений для работы (но может их отлавливать при необходимости).
  • Не зависит и не требует никаких внешних библиотек.
  • Не требует вручную регистрировать тесты (вот оно!).
  • Распространяется как набор заголовочных файлов и исходного кода (ну ещё препроцессор отдельно), не требуется сборка никаких библиотек.
Итак, как же с ним работать.
  1. Тесты организуются в наборы (suite). Каждый набор представляется классом, наследуемым от CxxTest::TestSuite. Тестами в наборе считаются все функции, имя которых начинается с "test" (в любом регистре, напимер test1, Test_Function).
  2. Если нужно делать инициализацию/деиницилизацию уровня набора, то в нём объявляются статические функции createSuite и destroySuite.
  3. Созданный заголовочный файл отдаётся генератору на Perl (cxxtestgen.pl) или Python (cxxtestgen.py), который генерирует исходный файл с точкой входа. В этот файл включается созданный заголовочный файл с тестами
  4. cxxtestgen.py --error-printer -o runner.cpp MyTestSuite.h
  5. Этот исходный файл потом собирается как угодно в приложение, которое и является тестом.
  6. Для тестов есть много разнообразных макросов с говорящими названиями: TS_ASSERT, TS_ASSERT_EQUALS, TS_ASSERT_DELTA или даже TS_ASSERT_THROWS_ANYTHING.
Итак, заголовочный файл может выглядеть примерно таким образом:
include <cxxtest h>

class MyTestSuite : public CxxTest::TestSuite
{
public:
void testAddition( void )
{
TS_ASSERT( 1 + 1 > 1 );
TS_ASSERT_EQUALS( 1 + 1, 2 );
}

void testMultiplication( void )
{
TS_ASSERT_EQUALS( 2 * 2, 5 );
}
};

Запустив этот "тест", можно увидеть:
# ./test
Running 2 tests.
test.h:15: Expected (2 * 2 == 5), found (4 != 5)
Failed 1 of 2 tests
Success rate: 50%

Кроме того, если передать генератору параметры --gui=Win32Gui или --gui=QtGui, то программа соберётся со страшным графическим интерфейсам из одного прогресс-бара. Мне подобное не нужно, но кому-то это может показаться удобно, например если тестов в файле много и они выполняются очень долго.

Интеграция с CMake


Так как я использую CMake в качестве системы сборки, то прикручивать тесты буду именно к ней.
В CMake нашелся модуль для "поиска" и использования CxxTest. Но модуль какой-то невнятный и неудобный — искать не умеет, исполняемые файлы создаёт только из получившегося после препроцессора исходного файла, то есть никаких других файлов ни прилинковать, никаких других исходников не добавить. Если тесты тянут по зависимостям функции из других файлов это недопустимо.
В итоге я немного переделал код, найденный в wiki сайта CxxTest и получил такой макрос:

SET(CXXTEST_EXECUTABLE ${PROJECT_SOURCE_DIR}/3rdparty/cxxtest/cxxtestgen.pl)

MACRO(unit_test NAME CXX_FILE FILES)
SET(PATH_FILES "")
# Мне это не нужно, но если файлы расположены в
# другом каталоге то может понадобиться
#FOREACH(part ${FILES})
# SET(PATH_FILES "${CMAKE_CURRENT_SOURCE_DIR}/${part}" ${PATH_FILES})
#ENDFOREACH(part ${FILES})
SET(PATH_FILES ${FILES})

SET(CXX_FILE_REAL "${CMAKE_CURRENT_SOURCE_DIR}/${CXX_FILE}")
SET(CXXTEST_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${NAME}.cxx")
ADD_CUSTOM_COMMAND(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${NAME}.cxx"
COMMAND ${CXXTEST_EXECUTABLE} --error-printer -o "${CXXTEST_OUTPUT}" ${CXX_FILE_REAL}
DEPENDS "${FILE}")
SET_SOURCE_FILES_PROPERTIES(${CXXTEST_OUTPUT} PROPERTIES GENERATED true)

ADD_EXECUTABLE("${NAME}" "${CXXTEST_OUTPUT}" ${PATH_FILES})
TARGET_LINK_LIBRARIES("${NAME}" ${CXXTEST_LINK_LIBS})
ADD_TEST("${NAME}" "${EXECUTABLE_OUTPUT_PATH}/${NAME}")
ENDMACRO(unit_test)

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

Интеграция с CTest


Команда ADD_TEST в макросе добавляет тест для CTest.
Чтобы CTest вообще работал, перед применением его команд в CMake надо вызвать ENABLE_TESTING().
Ну и для каждого заголовочного файла с тестом вызывать созданный макрос unit_test.
После сборки проекта, можно запустить тесты несколькими способами.
  1. make test — выполняет все тесты, выводит о них Passed или Failed. Это можно вставить в скрипты сборки и не вспоминать о них пока тесты не поломаются.
  2. Непосредственный запуск CTest: ctest, более подробный ctest -v или даже ctest -vv.
  3. Если CMake создаёт проект для Visual Studio, то создаётся отдельный проект для тестов, который можно запустить.
Всё хорошо, всё работает, осталось только приучить себя писать тесты...
Дополнительные материалы:

  1. Руководство CMake

  2. Руководство CTest

  3. Руководство CxxTest

Комментариев нет:

Отправить комментарий