В третьей и последней статье из серии статей об инструментах Open Source для модульного тестирования рассказывается о CppTest – простом и удобном фреймворке, предназначенном для создания модульных тестов, написанном Никласом Лунделлом (Niklas Lundell). Самое ценное свойство CppTest заключается в том, что этот фреймворк прост для понимания и использования. Из этой статьи вы узнаете, как с помощью CppTest создавать модульные тесты и тестовые пакеты, разрабатывать тестовые фикстуры и форматировать получаемые результаты, а также познакомитесь с несколькими полезными макросами из состава CppTest. Для опытных пользователей приводится сравнение фреймворков CppUnit и CppTest.
Фреймворк CppTest распространяется по лицензии GPL и доступен для бесплатной загрузки с Web-сайта Sourceforge (см. раздел Ресурсы). Компоновка исходного кода выполняется в обычном для open source формате configure-make. После его загрузки вы получите статическую библиотеку libcpptest. Клиентский код должен содержать заголовочный файл cppTest.h, который является частью загружаемого исходного кода, а также ссылку на статическую библиотеку libcpptest.a. Эта статья основана на CppTest версии 1.1.0.
Что представляет собой тестовый пакет?
Модульное тестирование означает проверку определенных фрагментов исходного кода. В простейшей форме тестирование заключается в том, что с помощью ряда определенных функций C/C++ проверяется другой код C/C++. Фреймворк CppTest определяет класс под названием Suite внутри пространства имен Test, содержащий базовые функции для модульного тестирования. Чтобы расширить эту функциональность путем определения дополнительных функций, необходимо использовать пользовательские тестовые пакеты. В листинге 1 определен класс с именем myTest, содержащий две функции, каждая из которых тестирует часть исходного кода. Для регистрации тестов предназначен макрос TEST_ADD.
Листинг 1. Расширение базового класса Test::Suite
#include “cppTest.h”
class myTest : public Test::Suite {
void function1_to_test_some_code( );
void function2_to_test_some_code( );
myTest( ) {
TEST_ADD (myTest::function1_to_test_some_code);
TEST_ADD (myTest::function2_to_test_some_code);
}
};
|
Можно легко расширить функциональность тестового пакета, создав иерархию тестовых пакетов. Такая необходимость вытекает из того факта, что каждый тестовый пакет мог бы проверять определенную область кода (например, выполнять синтаксический разбор, проверять генерацию и оптимизацию кода), а иерархическая структура упрощает управление тестами по прошествии времени. В листинге 2 показано, как можно создать такую иерархию.
Листинг 2. Создание иерархии модульных тестов
#include “cppTest.h”
class unitTestSuite1 : public Test::Suite { … }
class unitTestSuite2 : public Test::Suite { … }
class myTest : public Test::Suite {
myTest( ) {
add (std::auto_ptr<Test::Suite>(new unitTestSuite1( )));
add (std::auto_ptr<Test::Suite>(new unitTestSuite2( )));
}
};
|
Метод add принадлежит классу Suite. В листинге 3 показан его прототип (исходный код содержится в заголовке cpptest-suite.h).
Листинг 3. Объявление метода Suite::add
class Suite
{
public:
…
void add(std::auto_ptr<Suite> suite);
...
} ;
|
Метод run класса Suite отвечает за запуск тестов. Это показано в листинге 4.
Листинг 4. Запуск тестового пакета в подробном режиме
#include “cppTest.h”
class myTest : public Test::Suite {
void function1_to_test_some_code( ) { … };
void function2_to_test_some_code( ) { … };
myTest( ) {
TEST_ADD (myTest::function1_to_test_some_code);
TEST_ADD (myTest::function2_to_test_some_code);
}
};
int main ( )
{
myTest tests;
Test::TextOutput output(Test::TextOutput::Verbose);
return tests.run(output);
}
|
Метод run возвращает результат типа Boolean, который принимает значение True только в том случае, если все тесты завершились успешно. Аргументом метода run является объект типа TextOutput. Класс TextOutput управляет выводом результатов тестирования. По умолчанию информация выводится на экран.
Помимо подробного режима существует краткий режим. Разница между ними заключается в том, что в случае невыполнения утверждений в отдельных тестах в подробном режиме выводятся номера строк и имена файлов, тогда как в кратком режиме выводится лишь количество успешных и неудачных тестов.
Продолжение после неудачного теста
Что происходит, когда отдельный тест оканчивается неудачей? Решение о продолжении тестирования зависит исключительно от клиентского кода. По умолчанию выполнение остальных тестов продолжается. В листинге 5 показан прототип метода run.
Листинг 5. Прототип метода run
bool Test::Suite::run(
Output & output,
bool cont_after_fail = true
);
|
Второму аргументу метода run следует присваивать значение False в тех случаях, если после первого неудачного теста регрессия должна остановиться. Однако необходимость такого поведения может оказаться неочевидной. Предположим, что клиентский код пытается записать информацию на заполненный диск – в результате выполнения теста возникнет ошибка, и все последующие подобные тесты пакета также окончатся неудачей. В такой ситуации имеет смысл немедленно остановить регрессию.
Идея функций форматирования вывода заключается в том, чтобы при необходимости можно было выводить результаты тестирования в различных форматах: в текстовом виде, в виде HTML-страниц и т. д. Сам метод run не выводит никаких результатов, но зато он принимает отвечающий за их вывод объект типа Output. В CppTest доступны следующие три типа функций форматирования вывода:
Test::TextOutput. Простейший из обработчиков. Режим вывода результатов может быть подробным или кратким.Test::CompilerOutput. Вывод результатов генерируется подобно журналу работы компилятора.Test::HtmlOutput. Вывод в формате HTML.
По умолчанию все три функции выводят результат на устройство std::cout. Конструкторы первых двух функций принимают аргумент типа std::ostream, который определяет, куда необходимо выводить результаты (например, в файл с целью последующего использования). Также можно создать свою версию функции вывода. Единственным требованием для этого является обязательное наследование пользовательской функции вывода от класса Test::Output. Чтобы понять, чем отличаются различные форматы вывода, давайте рассмотрим код листинга 6.
Листинг 6. Запуск макроса TEST_FAIL в кратком режиме
#include “cppTest.h”
class failTest1 : public Test::Suite {
void always_fail( ) {
TEST_FAIL (“This always fails!\n”);
}
public:
failTest1( ) { TEST_ADD(failTest1::always_fail); }
};
int main ( ) {
failTest1 test1;
Test::TextOutput output(Test::TextOutput::Terse);
return test1.run(output) ? 1 : 0;
}
|
Обратите внимание на то, что TEST_FAIL является макросом, предопределенным в заголовке cppTest.h, что приводит к нарушению утверждения (это будет рассмотрено позже). В листинге 7 показаны результаты.
Листинг 7. Краткий формат вывода, показывающий только число неудачных тестов
failTest1: 1/1, 0% correct in 0.000000 seconds Total: 1 tests, 0% correct in 0.000000 seconds |
В листинге 8 показан вывод тех же результатов в подробном режиме.
Листинг 8. Подробный формат вывода, показывающий название файла и номер строки, сообщение, информацию о тестовом пакете и т. д.
failTest1: 1/1, 0% correct in 0.000000 seconds
Test: always_fail
Suite: failTest1
File: /home/arpan/test/mytest.cpp
Line: 5
Message: "This always fails!\n"
Total: 1 tests, 0% correct in 0.000000 seconds
|
В листинге 9 приведен код для вывода информации в стиле компилятора.
Листинг 9. Запуск макроса TEST_FAIL в режиме форматирования в стиле компилятора
#include “cppTest.h”
class failTest1 : public Test::Suite {
void always_fail( ) {
TEST_FAIL (“This always fails!\n”);
}
public:
failTest1( ) { TEST_ADD(failTest1::always_fail); }
};
int main ( ) {
failTest1 test1;
Test::CompilerOutput output;
return test1.run(output) ? 1 : 0;
}
|
Обратите внимание на сходство синтаксиса, приведенного в листинге 10, с файлом журнала компилятора GNU Compiler Collection (GCC).
Листинг 10. Краткий формат вывода, показывающий только количество неудачных тестов
/home/arpan/test/mytest.cpp:5: “This always fails!\n” |
По умолчанию вывод результатов в таком формате представляет собой журнал в стиле компилятора GCC. Тем не менее, можно также выводить результаты в форматах компиляторов Microsoft® Visual C++® и Borland. В листинге 11 генерируется журнал в стиле Visual C++, помещаемый в выходной файл.
Листинг 11. Запуск макроса TEST_FAIL в режиме форматирования в стиле компилятора
#include <ostream>
int main ( ) {
failTest1 test1;
std::ofstream ofile;
ofile.open("test.log");
Test::CompilerOutput output(
Test::CompilerOutput::MSVC, ofile);
return test1.run(output) ? 1 : 0;
}
|
В листинге 12 показано содержимое файла test.log, сгенерированного в результате выполнения кода листинга 11.
Листинг 12. Вывод результатов в стиле компилятора Virtual C++
/home/arpan/test/mytest.cpp (5) : “This always fails!\n” |
Наконец, рассмотрим код функции HtmlOutput, предназначенной для вывода результатов в HTML-формате. Обратите внимание на то, что функция этого типа не принимает дескриптор файла в конструктор, а вместо этого использует метод generate. Первым аргументом метода generate является объект типа std::ostream со значением по умолчанию std::cout (для получения подробной информации изучите исходный заголовочный файл cpptest-htmloutput.h). Можно использовать дескриптор файла для перенаправления журнала в любое место. Пример показан в листинге 13.
Листинг 13. Форматирование в стиле HTML
#include *<ostream>
int main ( ) {
failTest1 test1;
std::ofstream ofile;
ofile.open("test.log");
Test::HtmlOutput output( );
test1.run(output);
output.generate(ofile);
return 0;
}
|
В листинге 14 показан фрагмент HTML-вывода, помещенного в файл test.log.
Листинг 14. Фрагмент сгенерированного HTML-вывода
…
<table summary="Test Failure" class="table_result">
<tr>
<td style="width:15%" class="tablecell_title">Test</td>
<td class="tablecell_success">failTest1::always_fail</td>
</tr>
<tr>
<td style="width:15%" class="tablecell_title">File</td>
<td class="tablecell_success">/home/arpan/test/mytest.cpp:18</td>
</tr>
<tr>
<td style="width:15%" class="tablecell_title">Message</td>
<td class="tablecell_success">"This always fails!\n"</td>
</tr>
</table>
…
|
Отдельные модульные тесты, входящие в состав тестового пакета, часто имеют одинаковые требования к исходному состоянию: необходимо создавать объекты с определенными параметрами, открывать дескрипторы файлов и порты операционной системы и так далее. Вместо того чтобы дублировать одинаковый код в каждом методе, в таких случаях предпочтительнее использовать некоторые общие процедуры инициализации и завершения, и вызывать их для каждого теста. Для этого нужно определить методы setup и tear-down, сделав их частью тестового пакета. В листинге 15 определяется тестовый пакет myTestWithFixtures, использующий фикстуры.
Листинг 15. Создание тестового пакета с использованием фикстур
#include “cppTest.h”
class myTestWithFixtures : public Test::Suite {
void function1_to_test_some_code( );
void function2_to_test_some_code( );
public:
myTest( ) {
TEST_ADD (function1_to_test_some_code);
TEST_ADD (function2_to_test_some_code);
}
protected:
virtual void setup( ) { … };
virtual void tear_down( ) { … };
};
|
Заметьте, что не нужно каким-либо явным образом вызывать методы setup и tear-down. Нет необходимости объявлять эти процедуры в качестве виртуальных до тех пор, пока вы не захотите расширить тестовый пакет. Обе процедуры не должны принимать никаких аргументов, а возвращаемый результат должен иметь тип void.
В состав CppTest входят несколько полезных макросов, которые используются для проверки клиентского исходного кода. Эти макросы определены в файле cpptest-assert.h, ссылка на который присутствует в заголовке cpptest.h. Ниже будут рассмотрены некоторые из этих макросов, а также ситуации, в которых их можно использовать. Вывод результатов представлен в листингах в подробном режиме, если это не оговорено отдельно.
Макрос, представленный в листинге 16, предназначен для обозначения безусловной ошибки. Типичная ситуация, в которой можно использовать этот макрос – обработка результатов клиентской функции. Если результат не соответствует ожидаемому, вызывается исключение и сообщение об ошибке. При срабатывании макроса TEST_FAIL выполнение остального кода в данном модульном тесте прекращается.
Листинг 16. Клиентский код, использующий макрос TEST_FAIL
void myTestSuite::unitTest1 ( ) {
int result = usercode( );
switch (result) {
case 0: // Do Something
case 1: // Do Something
…
default: TEST_FAIL (“Invalid result\n”);
}
}
|
Этот макрос аналогичен библиотечной процедуре assert языка C за исключением того, что TEST_ASSERT работает как с отладочными, так и с конечными сборками. Если в результате проверки выражения получено значение False, генерируется признак ошибки. В листинге 17 показана внутренняя реализация этого макроса.
Листинг 17. Реализация макроса TEST_ASSERT
#define TEST_ASSERT(expr) \
{ \
if (!(expr)) \
{ \
assertment(::Test::Source(__FILE__, __LINE__, #expr)); \
if (!continue_after_failure()) return; \
} \
}
|
TEST_ASSERT_MSG (выражение, сообщение)
Этот макрос аналогичен макросу TEST_ASSERT за исключением того, что когда утверждение нарушено, в соответствующем месте выводится сообщение. Ниже используются утверждения с выводом и без вывода сообщения.
TEST_ASSERT (1 + 1 == 0); TEST_ASSERT (1 + 1 == 0, “Invalid expression”); |
В листинге 18 показан вывод результатов при нарушении утверждения.
Листинг 18. Результаты выполнения макросов TEST_ASSERT и TEST_ASSERT_MSG
Test: compare Suite: CompareTestSuite File: /home/arpan/test/mytest.cpp Line: 91 Message: 1 + 1 == 0 Test: compare Suite: CompareTestSuite File: /home/arpan/test/mytest.cpp Line: 92 Message: Invalid Expression |
TEST_ASSERT_DELTA (выражение 1, выражение 2, допустимый диапазон)
Если выражение 1 и выражение 2 различаются на величину, превышающую допустимый диапазон, то создается исключение. Этот макрос чрезвычайно полезен в тех случаях, когда выражение 1 и выражение 2 являются числами с плавающей запятой, например, в зависимости от способа округления, число 4,3 может храниться как 4,299999 или 4,300001, поэтому, чтобы сравнение работало, необходимо задать определенный допустимый диапазон. Другим примером является тестирование кода для операций ввода/вывода: время открытия файла не может каждый раз быть одинаковым, но не должно превышать определенный интервал.
TEST_ASSERT_DELTA_MSG (выражение, сообщение)
Этот макрос аналогичен макросу TEST_ASSERT_DELTA за исключением того, что при нарушении утверждения дополнительно выводится сообщение.
TEST_THROWS (выражение, исключение)
Этот макрос проверяет выражение и ожидает исключение. Если исключение не получено, срабатывает утверждение. Заметьте, что фактическое значение выражения не помещается в какой-либо тест – проверяется исключение. Рассмотрим код листинга 19.
Листинг 19. Обработка целочисленных исключений
class myTest1 : public Test::Suite {
…
void func1( ) {
TEST_THROWS (userCode( ), int);
}
public:
myTest1( ) { TEST_ADD(myTest1::func1); }
};
void userCode( ) throws(int) {
…
throw int;
}
|
Обратите внимание на то, что возвращаемый тип функции userCode может быть как double, так и integer. Поскольку здесь функция userCode безоговорочно вызывает исключение типа int, тест завершится успешно.
TEST_THROWS_ANYTHING (выражение)
Иногда, в зависимости от ситуации, клиентская процедура вызывает исключения различных типов. Для обработки таких ситуаций существует макрос TEST_THROWS_ANYTHING, в котором не указывается ожидаемый тип исключения. До тех пор, пока после выполнения клиентского кода будет возникать какое-либо исключение, утверждение не будет срабатывать.
TEST_THROWS_MSG (выражение, исключение, сообщение)
Этот макрос аналогичен макросу TEST_THROWS за исключением того, что в этом случае выводится сообщение, а не выражение. Рассмотрим следующий код:
TEST_THROWS(userCode( ), int); TEST_THROWS(userCode( ), int, “No expected exception of type int”); |
В листинге 20 показан вывод макросов при нарушении утверждения.
Листинг 20. Вывод макросов TEST_THROWS и TEST_THROWS_MSG
Test: func1 Suite: myTest1 File: /home/arpan/test/mytest.cpp Line: 24 Message: userCode() Test: func2 Suite: myTest1 File: /home/arpan/test/mytest.cpp Line: 32 Message: No expected exception of type int |
Сравнение фреймворков CppUnit и CppTest
Во второй части этой серии был рассмотрен CppUnit – другой популярный open source фреймворк для модульного тестирования. Фреймворк CppTest намного проще по сравнению с CppUnit, однако со своей задачей он справляется. Ниже представлено сравнение этих двух инструментов по различным параметрам.
- Легкость создания модульных тестов и тестовых пакетов. И
CppUnit, иCppTestсоздают модульные тесты из методов класса, а сам класс наследуется из некоторого встроенного классаTest. Однако синтаксисCppTestнемного проще, поскольку регистрация тестов происходит внутри конструктора класса. В случае работы сCppUnitнеобходимо дополнительно использовать макросыCPPUNIT_TEST_SUITEиCPPUNIT_TEST_SUITE_ENDS. - Запуск тестов.
CppTestпросто вызывает методrunв тестовом пакете, тогда какCppUnitиспользует отдельный классTestRunnerи выполняет запуск тестов с помощью методаrunэтого класса. - Расширение иерархии тестирования. В случае работы с
CppTestвсегда можно расширить предыдущий тестовый пакет, создав новый класс, наследуемый от старого. В новом классе определяются некоторые дополнительные функции, добавляемые в набор модульных тестов. Затем просто вызывается методrunв объекте, имеющем тип нового класса. Для получения того же эффекта вCppUnit, наряду с наследованием класса, требуется использовать макросCPPUNIT_TEST_SUB_SUITE. - Форматирование вывода. И
CppTest, иCppUnitимеют возможность настройки отображения результатов.CppTestсодержит полезную предопределенную функцию для HTML-форматирования, тогда как вCppUnitтакой функции нет. ОднакоCppUnitимеет исключительную поддержку формата XML. Оба фреймворка поддерживают текстовый формат и формат компилятора. - Создание тестовых фикстур. Для использования текстовых фикстур в
CppUnitтребуется, чтобы тестовый класс являлся производным от классаCppUnit::TestFixture, а также необходимо задать определения для процедур setup и tear-down. При использованииCppTestнеобходимо лишь задать определения для процедур setup и tear-down. Это определенно лучшее решение, поскольку оно не усложняет клиентский код. - Поддержка предопределенных макросов. И
CppTest, иCppUnitимеют сопоставимый набор макросов для работы с утверждениями, обработки чисел с плавающей запятой и т. д. - Заголовочные файлы.
CppTestтребует подключения одного заголовочного файла, а клиентский кодCppUnitдолжен включать несколько заголовочных файлов, таких как HelperMacros.h и TextTestRunner.h, в зависимости от используемых функций.
На сегодняшний день модульное тестирование является фундаментальной частью процесса разработки программного обеспечения. Фреймворк CppTest является еще одним инструментом разработчика C/C++ для тестирования кода, в результате чего упрощается его поддержка и сопровождение. Интересно обратить внимание на то, что все три фреймворка, рассмотренные в этой серии статей – Boost, CppUnit и CppTest – используют одни и те же базовые концепции, такие как фикстуры, макросы для утверждений и функции форматирования вывода. Все эти инструменты разработаны на базе открытого кода, поэтому вы можете изменить их в соответствии с вашими потребностями (например, добавить поддержку XML-формата для CppTest).
Научиться
-
Оригинал статьи Open source C/C++ unit testing tools, Part 3: Get to know CppTest (EN).
- Посетите страницу проекта
CppTest(EN) на сайте Sourceforge.com. -
Следите на последними новостями на портале Web-трансляций и технических мероприятий developerWorks (EN).
-
Статья Open source C/C++ unit testing tools, Part 1: Get to know the Boost unit test framework (developerWorks, декабрь 2009 г.) (EN), в которой рассказывается о фреймворке Boost, предназначенном для модульного тестирования продуктов, написанных на языке C/C++.
-
Статья Open source C/C++ unit testing tools, Part 2: Get to know CppUnit (developerWorks, январь 2010 г.) (EN), в которой рассказывается о CppUnit – версии фреймворка JUnit, портированной для C/C++.
Получить продукты и технологии
- Загрузите последнюю версию
CppTestна странице загрузки CppTest (EN).
Арпан Сен (Arpan Sen) – ведущий инженер, работающий над разработкой программного обеспечения в области автоматизации электронного проектирования. На протяжении нескольких лет он работал над некоторыми функциями UNIX, в том числе Solaris, SunOS, HP-UX и IRIX, а также Linux и Microsoft Windows. Он проявляет живой интерес к методикам оптимизации производительности программного обеспечения, теории графов и параллельным вычислениям. Арпан является аспирантов в области программных систем.