Инструменты Open Source для модульного тестирования C/C++: Часть 3. Знакомство с CppTest

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

Арпан Сен, технический директор, Synapti Computer Aided Design Pvt Ltd

Арпан Сен (Arpan Sen) – ведущий инженер, работающий над разработкой программного обеспечения в области автоматизации электронного проектирования. На протяжении нескольких лет он работал над некоторыми функциями UNIX, в том числе Solaris, SunOS, HP-UX и IRIX, а также Linux и Microsoft Windows. Он проявляет живой интерес к методикам оптимизации производительности программного обеспечения, теории графов и параллельным вычислениям. Арпан является аспирантов в области программных систем.



16.01.2012

В третьей и последней статье из серии статей об инструментах Open Source для модульного тестирования рассказывается о CppTest – простом и удобном фреймворке, предназначенном для создания модульных тестов, написанном Никласом Лунделлом (Niklas Lundell). Самое ценное свойство CppTest заключается в том, что этот фреймворк прост для понимания и использования. Из этой статьи вы узнаете, как с помощью CppTest создавать модульные тесты и тестовые пакеты, разрабатывать тестовые фикстуры и форматировать получаемые результаты, а также познакомитесь с несколькими полезными макросами из состава CppTest. Для опытных пользователей приводится сравнение фреймворков CppUnit и CppTest.

Часто используемые сокращения

  • HTML: Hypertext Markup Language – язык гипертекстовой разметки.
  • I/O: Input/output – ввод/вывод.
  • XML: Extensible Markup Language – расширяемый язык разметки.

Установка и использование

Фреймворк 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 входят несколько полезных макросов, которые используются для проверки клиентского исходного кода. Эти макросы определены в файле cpptest-assert.h, ссылка на который присутствует в заголовке cpptest.h. Ниже будут рассмотрены некоторые из этих макросов, а также ситуации, в которых их можно использовать. Вывод результатов представлен в листингах в подробном режиме, если это не оговорено отдельно.

TEST_FAIL (сообщение)

Макрос, представленный в листинге 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”); 
   }
}

TEST_ASSERT (выражение)

Этот макрос аналогичен библиотечной процедуре 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).

Ресурсы

Научиться

Получить продукты и технологии

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=787746
ArticleTitle=Инструменты Open Source для модульного тестирования C/C++: Часть 3. Знакомство с CppTest
publish-date=01162012