В прошлый раз, когда я пытался приучить себя к тестам, меня отпугнула замороченость CppUnit (клона jUnit для C++), поэтому в этот раз я начал с обзора других средств для тестирования кода на C++.
Вообще я немного поковырял googletest, UnitTest++, и даже Boost Test Library, но речь сейчас не о них.
В конце концов, я искал простое и функциональное решение, безо всяких дурацких регистраций тестов и прочей ненужной синтаксической ерунды.
В конце концов я выбрал CxxTest.
CxxTest
Основные преимущества:
- Не требует RTTI и шаблонов.
- Не требует механизма исключений для работы (но может их отлавливать при необходимости).
- Не зависит и не требует никаких внешних библиотек.
- Не требует вручную регистрировать тесты (вот оно!).
- Распространяется как набор заголовочных файлов и исходного кода (ну ещё препроцессор отдельно), не требуется сборка никаких библиотек.
- Тесты организуются в наборы (suite). Каждый набор представляется классом, наследуемым от CxxTest::TestSuite. Тестами в наборе считаются все функции, имя которых начинается с "test" (в любом регистре, напимер test1, Test_Function).
- Если нужно делать инициализацию/деиницилизацию уровня набора, то в нём объявляются статические функции createSuite и destroySuite.
- Созданный заголовочный файл отдаётся генератору на Perl (cxxtestgen.pl) или Python (cxxtestgen.py), который генерирует исходный файл с точкой входа. В этот файл включается созданный заголовочный файл с тестами
- Этот исходный файл потом собирается как угодно в приложение, которое и является тестом.
- Для тестов есть много разнообразных макросов с говорящими названиями: TS_ASSERT, TS_ASSERT_EQUALS, TS_ASSERT_DELTA или даже TS_ASSERT_THROWS_ANYTHING.
cxxtestgen.py --error-printer -o runner.cpp MyTestSuite.h
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.
После сборки проекта, можно запустить тесты несколькими способами.
- make test — выполняет все тесты, выводит о них Passed или Failed. Это можно вставить в скрипты сборки и не вспоминать о них пока тесты не поломаются.
- Непосредственный запуск CTest: ctest, более подробный ctest -v или даже ctest -vv.
- Если CMake создаёт проект для Visual Studio, то создаётся отдельный проект для тестов, который можно запустить.
Дополнительные материалы: