суббота, 12 сентября 2009 г.

Исследование фреймворков для Юнит тестирования на C++

Одной из моих предыдущих тем обсуждения было сказано о применимости методики разработки через тестирование (Test Driven Development) для игровых приложений. Каждый раз эта тема всплывает в обсуждениях и почтовых рассылках. Почти каждый проявляет неимоверное любопытство об этой методике и хочет узнать об этом еще больше. Обещаю в скором времени об этом написать.

Но теперь я занят выбором фреймворка для юнит-тестов, для того, что бы использовать его в моей команде. Поэтому, прежде чем я заведу разговор о том, как использовать методику разработки через тестирование на примере игровых приложений, или о значимости юнит-тестов, или чему нибудь еще на эту тему, мы погрузимся в детальное сравнение существующих фреймворков для C++. Здесь нам придется задержаться. Путь, который нам предстоит пройти, обещает быть длинным и ухабистым с логичным завершением в конце.

В данной статье будут рассмотрены следующие Фреймворки:

CppUnit

Boost.Test

CppUnitLite

NanoCppUnit

Unit++

CxxTest

Вместо введения

Как мы выбираем фреймворк для юнит-тестов?Все зависит от того как мы собираемся им пользоваться, и для чего мы собираемся его использовать. Если бы я использовал Java для своей работы, выбор был бы очевиден - JUnit – оптимальный выбор для Java программистов. Я не слышал, что бы программисты обсуждали и предлагали что-то новое, поэтому данный фреймворк, похоже, лучшее, что сейчас существует для Java.

К сожалению, в случае C++ дела обстоят несколько иначе. Существует представитель семейства XUnit, CppUnit, но он не достаточно хорош в отдельных случаях. Существуют также другие фреймворки, которые можно использовать. Также, многие команды используют свои собственные изобретения. Почему так? Может C++ не совсем отвечает требованиям для разработки юнит-тестов, из-за чего имеются проблемы при использовании XUnit для этого языка?Не похоже, что это так. Очевидно лишь одно: разнообразие - это хорошо. Иначе при написании статьи мне пришлось бы использовать Windows, а вам для чтения использовать только Internet Explorer. В любом случае, я не первый кто задается этим вопросом. Эта страница пытается ответить на этот вопрос, и приходит к закономерным и логичным выводам: различия в компиляторах, платформах и стилях программирования. C++ не достаточно однозначен, он не является полностью поддерживаемым языком с единым стандартом кодирования.

Хорошей отправной точкой может послужить создание списка возможностей, которые имеют большое значение для того типа работы, которую я собираюсь сделать. В частности, я хочу заняться разработкой через тестирование (TDD), которая подразумевает, что я собираюсь написать и выполнить много маленьких тестов. Их планируется использовать в разработке игровых приложений, соответственно мне бы хотелось выполнить тесты на различных платформах (PC, Xbox, PS2, консолях следующего поколения и т.д.) Они должны соответствовать моему персональному стилю TDD (много тестов, массовое использование наработок и пр.)

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

  • Минимальное количество работы, требуемое для создания нового теста. Я собираюсь создавать тесты постоянно, следовательно, мне не хотелось бы, что бы добавление теста сопровождалось длительным процессом набора теста на клавиатуре, а в особенности, было бы неплохо избавить себя он необходимости дублирования исходного кода. Краткость и простота написания сделают возможным более прозрачным рефакторинг исходного кода, которые в случае TDD имеет огромное значение.
  • Простота в модификации и портировании. Не должно быть как-то зависимостей от нестандрартных библиотек или каких то «экзотических» особенностей языка (RTTI, обработка исключений и пр.). Некоторые компиляторы, которые используются для консольной разработки далеко не современны. Что бы убедиться в этом, я создавал юнит-тесты для каждого из фреймворков под Linux с использованием g++. Большинство тестов было написаны с учетом возможности их использования под Windows и Visual Studio.
  • Поддержка установки/удаления шагов (установщики(fixtures)). Я перенял стиль, рекомендованный Девидом Астелсом в его книге Test Driven Development, Практическом руководстве об использовании только одного утверждения на один тест. Это действительно делает тесты несколько проще для понимания и поддержки, но с другой стороны возрастает необходимость в тяжелом использовании установщиков. Фреймворки без таковых вычеркиваются незамедлительно. Дополнительным плюсом для фреймворков является возможность объявления объектов используемых в установщиках на стеке (или же создающиеся непосредственно перед вызовом теста) в противовес необходимости выделять их динамически.
  • Хорошая обработка исключений и крешей. Нам не хотелось бы останавливать тесты только из-за того, что какой то код пытается получить доступ к некорректному адресу памяти или в случае деления на нуль. Фреймворк для юнит-тестов должен информировать об возникающих исключительных ситуациях настолько детально, насколько это возможно. Должна также существовать возможность запускать выполнение повторно и иметь возможность установить брейкпоинт в месте возникновения исключительной ситуации.
  • Хорошая функциональность ASSERT. Ложное тестовое выражение должно распечатывать содержимое переменных, которые вовлечены в данное сравнение. Также должно существовать расширенное множество выражений проверки для случая «практически равно» (необходимо для переменных с плавающей запятой), меньше чем, больше чем и пр. Дополнительным плюсом будет являться наличие проверки на предмет, было выброшено исключение или нет.
  • Поддержка различных методов вывода информации. По умолчанию мне бы хотелось иметь формат, который был бы понятен для таких IDE, как Visual Studio или Kdevelop, тогда было бы просто определять непройденные тесты, когда имеют место синтаксические ошибки. Но я также хотел бы иметь возможность различных методов вывода информации (более детализированные, более короткие, более читабельные и пр.)
  • Поддержка TestSuit'ов. Этот пункт имеет очень маленький приоритет в моем списке, в то время как о нем заявлено в списках возможностей большинства фреймворков. Откровенно говоря, у меня было мало необходимости в данной возможности в прошлом. Да, это хорошо, но я рассмотрел много библиотек, каждая из которых имеет собственное множество тестов, таким образом, я едва в этом нуждаюсь. Тем не менее, было бы хорошо иметь окружение для запуска тестов в заданной точке выполнения.

Что еще: поддержка тайминга. Как для определения общего времени выполнения тестов, так и для каждого теста в отдельности. Мне нравится, когда я знаю, что сколько времени выполняется. Не столько в целях производительности, сколько для того, что бы избежать ситуаций, когда выполнение может быть неконтролируемым. Я предпочитаю, что бы выполнение тестов ограничивалось 3-4 секундами (это логичное желание, если необходимо запускать тест очень часто). В идеале, было бы неплохо, что бы выводилось предупреждение, когда выполнение теста занимает больше времени, чем допустимо.

Простота установки не учитывалась в списке приоритетов; в конце концов, это делается лишь один раз – а создание новых тестов занимает целые дни. Некомереческие лицензии (GPL или LGPL) так же не были учтены в моем списке требований, потому что фреймворк для юнит-тестов мы можем не включать в нашу конечную программу, таким образом, у нас не будет каких-либо ограничений из-за этого в финальной версии создаваемого приложения.

Кстати, в ходе моих исследований для этой статьи, я обнаружил, что список подобных требований был составлен уже до меня. Интересно сравнить эти требования и пожелания с моими.

Идеальный Фреймворк

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

Максимально простой тест должен быть тривиален. Лишь одна строка для создания теста и дальше само тело теста:

TEST (SimplestTest)

{

float someNum = 2.00001f;

ASSERT_CLOSE (someNum, 2.0f);

}

Тест с установщиком получается немного сложнее, но, тем не менее, он так же должен быть достаточно простым.

SETUP (FixtureTestSuite)

{

float someNum = 2.0f;

std::string str = "Hello";

MyClass someObject("somename");

someObject.doSomethng();

}

TEARDOWN (FixtureTestSuite)

{

someObject.doSomethingElse();

}

TEST (FixtureTestSuite, Test1)

{

ASSERT_CLOSE (someNum, 2.0f); someNum = 0.0f;

}

TEST (FixtureTestSuite, Test2)

{

ASSERT_CLOSE (someNum, 2.0f);

}

TEST (FixtureTestSuite, Test3)

{

ASSERT_EQUAL(str, "Hello");

}

Первым делом, что следует указать относительно этих тестов, это то, что здесь представлен только минимальный объем кода, отвечающий только за реализацию самого теста. Самый простой, максимально упрощенный тест занимает несколько строк и не требует никакой другой поддержки, кроме главного файла который запускает все тесты. Инициализация установщика с вызовами установки/удаления должно быть также максимально простым. Я не хочу наследоваться от каких либо классов, перегружать функции или что-то еще. Только написать шаг установки и двигаться дальше.

Посмотрите еще раз на функцию установки. Переменные используемые в тестах не создаются динамически. Вместо этого, вместо этого они объявляются на стеке и используются непосредственно оттуда. В дополнение к этому, я должен указать, что эти объекты должны создаваться только непосредственно перед каждым тестом, а не перед запуском всех тестов. Как именно это должно быть реализовано? Это что-то вроде того, что бы я хотел использовать. Поэтому это и называется - идеальный фреймворк.

А сейчас давайте посмотрим на 6 реальных фреймворков для юнит-тестирования, которые должны компилироваться и выполнятся. Для каждого из фреймворков, я провожу анализ на соответствие моему списку требований и пытаюсь реализовать те два теста, которые я написал для идеального фреймворка. Здесь можно найти исходный код для всех примеров.

CppUnit

CppUnit вероятно наиболее распространенный фреймворк для юнит-тестов на C++, поэтому будет логичным сравнивать его с другими системами тестирования. Я использовал CppUnit три или четыре года назад и мои впечателния были менее приятными. Я помню, мой код вперемешку с кодом MFC составлял какую-то непонятную чачу, все примеры в фреймворке были запутаны, и ничтожная GUI панель кое-как взаимодействовала с программой. Я даже было создал патч, который обеспечивал консольный вывод и убирал зависимости от MFC. Теперь же я возвращаюсь к данному Фреймворку, уже зная его слабые места.

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

  • Минимальное количество работы, требуемое для создания нового теста. Это один из самых больших проблем CppUnit, которое, по иронии, является самой важной для меня. CppUnit требует проделать немного работы, для того что бы создать примитивный тест.

// Simplest possible test with CppUnit

#include

class SimplestCase : public CPPUNIT_NS::TestFixture

{

CPPUNIT_TEST_SUITE( SimplestCase );

CPPUNIT_TEST( MyTest );

CPPUNIT_TEST_SUITE_END();

protected:

void MyTest();

};

CPPUNIT_TEST_SUITE_REGISTRATION( SimplestCase );

void SimplestCase::MyTest()

{

float fnum = 2.00001f;

CPPUNIT_ASSERT_DOUBLES_EQUAL( fnum, 2.0f, 0.0005 );

}

  • Простота в модификации и портировании. По этому поводу здесь двойственное ощущение. С одной стороны, CppUnit работает на Windows и Linux, и функциональность хорошо смодулирована (результаты, исполнители (runners), данные вывода). С другой стороны, CppUnit по-прежнему требует RTTI, STL и (я думаю) механизм обработки ошибок. Конечно, это не конец света, что данные технологии требуются, но очевидно, что если я захочу использовать библиотеки, которые не поддерживают RTTI или откажусь о STL, то у меня появятся проблемы.
  • Поддержка установщиков. Да. Если вы хотите, что бы объекты создавались перед каждым тестом, необходимо, что бы они были динамически созданы в setup(), таким образом бонуса здесь нету.

#include

#include "MyTestClass.h"

class FixtureTest : public CPPUNIT_NS::TestFixture

{

CPPUNIT_TEST_SUITE( FixtureTest );

CPPUNIT_TEST( Test1 );

CPPUNIT_TEST( Test2 );

CPPUNIT_TEST( Test3 );

CPPUNIT_TEST_SUITE_END();

protected:

float someValue;

std::string str;

MyTestClass myObject;

public:

void setUp();

protected:

void Test1();

void Test2();

void Test3();

};

CPPUNIT_TEST_SUITE_REGISTRATION( FixtureTest );

void FixtureTest::setUp()

{

someValue = 2.0;

str = "Hello";

}

void FixtureTest::Test1()

{

CPPUNIT_ASSERT_DOUBLES_EQUAL( someValue, 2.0f, 0.005f );

someValue = 0;

//System exceptions cause CppUnit to stop dead on its tracks

//myObject.UseBadPointer();

// A regular exception works nicely though myObject.ThrowException();

}

void FixtureTest::Test2()

{

CPPUNIT_ASSERT_DOUBLES_EQUAL( someValue, 2.0f, 0.005f );

CPPUNIT_ASSERT_EQUAL (str, std::string("Hello"));

}

void FixtureTest::Test3()

{

// This also causes it to stop completely

//myObject.DivideByZero();

// Unfortunately, it looks like the framework creates 3 instances of MyTestClass

// right at the beginning instead of doing it on demand for each test. We would

// have to do it dynamically in the setup/teardown steps ourselves.

CPPUNIT_ASSERT_EQUAL (1, myObject.s_currentInstances);

CPPUNIT_ASSERT_EQUAL (3, myObject.s_instancesCreated);

CPPUNIT_ASSERT_EQUAL (1, myObject.s_maxSimultaneousInstances);

}

  • Хорошая обработка ошибок и крешей. Да. CppUnit использует концепцию «протекторов» (protectors), которые являются обертками над тестами. Встроенный протектор перехватывает все исключения (и идентифицирует некоторые из них). Вы можете написать ваш собственный протектор и поместить его в стек, для того, что бы скомпоновать его с уже существующими. Я не обрабатывал системные исключения под Linux, но с добавлением нового протектора у меня не было проблем. Похоже, что тут есть возможность отключить обработку исключений и позволить отладчику остановить выполнение, когда возникнет исключение (нет дефайнов или параметров коммандной строки).
  • Хорошая функциональность ASSERT. Хорошая. Здесь есть минимальный набор выражений ASSERT, включающий работу с числами с плавающей запятой. Нету выражений для «меньше чем», «больше чем» и т.д. Содержимое переменных, которые сравниваются, выводится в поток, если ASSERT выполняется, предоставляя вам, столько информации, насколько это возможно о неудачном выполнении теста.
  • Поддержка различных методов вывода информации. Да. Имеется очень хорошо определенная функциональсть для вывода информации (содержит результаты выполнения теста), которая работает также хорошо и для слушателей (listener) (которые уведомляются в момент выполнения конкретного теста). Вывод информации имеет хороший формат, который замечательно интегрирован в Visual Studio. Также имеется поддержка прогресс баров.
  • Поддержка TestSuit'ов. Да.

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

Boost.Test

Обновлено: Я пересмотрел свой рейтинг и коментарии к фреймворку Boost.Test в свете комментариев Геннадия Розенталя, который показал мне, как просто добавлять установщик в Boost.

Я большой фанат Boost'а, но я хочу отметить, что это было не так год назад, до того как я узнал, что Boost предоставляет библиотеку для юнит-тестирования. Несомненно, я должен был в ней разобраться.

Первый сюрприз заключается в том, что Boost.Test не только фреймворк для юнит-тестирования. Он так же содержит в себе другие инструменты для тестирования. Ничего плохого в этом нету, но для меня это первый знак «дурного запаха». Другим сюрпризом является тот факт, что данный фреймворк не основан на семействе Xunit. Хммм.. В этом случае, он должен лучше предоставлять функциональность.

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

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

#include <boost/test/auto_unit_test.hpp>

#include

BOOST_AUTO_UNIT_TEST (MyFirstTest)

{

float fnum = 2.00001f;

BOOST_CHECK_CLOSE(fnum, 2.f, 1e-3);

}

  • Простота в модификации и портировании. Фреймворк получил смешанную оценку по данному пункту, по тем же причинам, что и CppUnit. Являясь частью библиотеки Boost, к портированию здесь подошли очень серьезно. Фреймворк работает безупречно на Linux (лучше чем большинство фреймворков). Но у меня есть вопрос по поводу того, насколько легко в самом деле добраться до исходного кода и начать делать изменения. Также случается, что необходимо добавить поддержку некоторых дополнительных заголовочных файлов из других библиотек Boost, таким образом, все вместе это уже не выглядит таким простым и самодостаточным.
  • Поддержка установщиков. Boost.Test не содержит структуры установки/удаления, подобной той, что имеется в тестах CppUnit в стиле конструкторов/деструкторов простого C++. Поначалу это притормозила скорость моего исследования. После длительного использования подхода установки/удаления и удобной комплексной установки, я не видел обычного пути использования построения установщиков. Теперь я разобрался с данным фреймворком и пришел к выводу, что это даже лучше привычного мне подхода установки/удаления. Одним из наиболее значимых преимуществ данного подхода, это то, что вам не нужно создавать объект установщика динамически, а вместо этого вы можете поместить установщик на стеке. С другой стороны, эта необходимость ссылаться на переменные в установщике через имя объекта несколько раздражает. Было бы лучше, если бы они могли бы появиться каким то магическим образом в той же области видимости, что и сам тест. Также было бы немного понятнее, если бы установщик, мог бы быть установленным на стеке с помощью макроса BOOST_AUTO_UNIT_TEST вместе необходимости явно помещать его на стек для каждого теста.

#include

#include

#include "MyTestClass.h"

struct MyFixture

{

MyFixture()

{

someValue = 2.0;

str = "Hello";

}

float someValue;

std::string str;

MyTestClass myObject;

};

BOOST_AUTO_UNIT_TEST (TestCase1)

{

MyFixture f;

BOOST_CHECK_CLOSE (f.someValue, 2.0f, 0.005f);

f.someValue = 13;

}

BOOST_AUTO_UNIT_TEST (TestCase2)

{

MyFixture f;

BOOST_CHECK_EQUAL (f.str, std::string("Hello"));

BOOST_CHECK_CLOSE (f.someValue, 2.0f, 0.005f);

// Boost deals with this OK and reports the problem

//f.myObject.UseBadPointer();

// Same with this

//myObject.DivideByZero();

}

BOOST_AUTO_UNIT_TEST (TestCase3)

{

MyFixture f;

BOOST_CHECK_EQUAL (1, f.myObject.s_currentInstances);

BOOST_CHECK_EQUAL (3, f.myObject.s_instancesCreated);

BOOST_CHECK_EQUAL (1, f.myObject.s_maxSimultaneousInstances);

}

  • Хорошая обработка ошибок и крешей. Это один аспект, где Boost.Test на голову перепрыгнул всех своих конкурентов. Он не только обрабатывает исключения корректно, но и печатает некоторую информацию о них, он перехватывает системные исключения Linux, и даже имеет аргументы командной строки, которые запрещают обработку исключений, позволяя таким образом решать данные проблемы в вашем отладчике при повторном запуске. В этом плане, мне действительно здесь больше нечего и добавить.
  • Хорошая функциональность ASSERT. Да. В фреймворке имеются подобные выражения для практически любых операций, которые только могут понадобиться (равенство, приближение, меньше чем, больше чем, битовое равенство и т.д.). Кроме того, имеется поддержка проверок, было ли выброшено исключение. Выражения ASSERT корректно выводят содержимое проверяемых переменных. Соответственно, здесь можно поставить наивысшую оценку.
  • Поддержка различных методов вывода информации. Вероятно, но не так-то просто это реализовать. Как минимум, вывод по умолчанию достаточно комфортен. Я подозреваю, что мне нужно было поглубже поковыряться в unit_test_log_formatter, но я, разумеется, не видел разнообразие возможных вариантов вывода информации, которые можно было бы встроить сюда.
  • Поддержка TestSuit'ов. Да, но с большой уловкой. Если я ничего не упустил (что вполне возможно, если так и есть, то дайте знать), создание TestSuit требует приличного количества дополнительного кода и также требует модификации исполнителей (runner) в main. Взгляните на пример. Не могло бы ли это быть попроще? Не то, что бы это было большой проблемой - это требование для меня наименее важное, но мне бы хотелось помечать все тесты в одном файле, как часть TestSuit простым макросом в начале файла. Другая неприятная мелочь это отсутствие шагов установки/удаления для всего TestSuit, что могло бы действительно быть полезным.

#include <boost/test/unit_test.hpp>

#include <boost/test/floating_point_comparison.hpp>

using boost::unit_test::test_suite;

struct MyTest

{

void TestCase1()

{

float fnum = 2.00001f;

BOOST_CHECK_CLOSE(fnum, 2.f, 1e-3);

}

void TestCase2()

{}

};

test_suite * GetSuite1()

{

test_suite * suite = BOOST_TEST_SUITE("my_test_suite");

boost::shared_ptr instance( new MyTest() );

suite->add (BOOST_CLASS_TEST_CASE( &MyTest::TestCase1, instance ));

suite->add (BOOST_CLASS_TEST_CASE( &MyTest::TestCase2, instance ));

return suite;

}

#include

using boost::unit_test::test_suite;

extern test_suite * GetSuite1();

boost::unit_test::test_suite* init_unit_test_suite( int /* argc */, char* /* argv */ [] )

{

test_suite * test = BOOST_TEST_SUITE("Master test suite");

test->add( boost::unit_test::ut_detail::auto_unit_test_suite() );

test->add(GetSuite1());

return test;

}

Boost.Test это библиотека с огромным потенциалом. Она имеет огромную поддержку обработки исключений и расширенный набор инструкции ASSERT. Также можно отметить наличие уникальной функциональности по проверке на зацикливание, и различные уровни логгирования. С другой стороны, использование библиотеки очень многословно в случае добавления нового теста в качестве части TestSuit, и это может быть сдерживающим фактором для консольного игрового окружения.

CppUnitLite

У CppUnitLite интересная история возникновения. Михаель Феверс (Michael Feathers), автор CppUnit был несколько расстроен сложностью CppUnit и тем что фреймворк не удовлетворяет все нужды пользователей, и поэтому решил создать ультра легкий фреймворк CppUnitLite. Легкий, как в плане возможностей, так и в плане сложности и размера, но его философия заключалась в том, чтобы позволить пользователем конфигурировать его в соответствии с их потребностями.

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

С другой стороны, у CppUnitLite нету никакой документации. Черт возьми, у фреймворка даже нету сайта, что, я уверен, является серьезной проблемой его распространения.

  • Минимальное количество работы, требуемое для создания нового теста. Абсолютно! Из всех рассмотренных фреймворков, CppUnitLite наиболее близко подошел к уровню идеального в этом аспекте. С другой стороны, необходимо учесть факт, что я больше всего использовал для своих целей CppUnitLite и это оказало влияние на субъективизм моей оценки. Так или иначе, фреймворк реализует мою идею о минимально требуемых усилиях для создания простого теста и даже с возможностью использования установщика (хотя все равно, я уверен, что можно сделать лучше).

#include "lib/TestHarness.h"

TEST (Whatever,MyTest)

{

float fnum = 2.00001f;

CHECK_DOUBLES_EQUAL (fnum, 2.0f);

}

  • Простота в модификации и портировании. Без сомнений. Опять таки, Высшая оценка в данной категории. Другие фреймворки не могут соревноваться в этом с CppUnitLite по своей простоте в легкости модификации и портировании, и в то же время отвечающие требованиям разделении функциональности. Оригинальная версия CppUnitLite имеет в своем распоряжении простой и легкий класс для представления строк, избавляя таким образом от зависимости от STL. В моей модифицированной версии, я добавил поддержку std::string, так как я использую его в большинстве своих проектов, но это изменение требует не больше минуты. Также, использование в Linux абсолютно тривиально, несмотря на то, что я ранее использовал фреймворк только под Windows.
  • Поддержка установщиков. Это как раз то место, где у оригинального CppUnitLite есть проблемы. Фреймворк настолько легковесный, что он не поддерживает множество особенностей. Данная функциональность критична для меня, и я, таким образом, добавил ее.

#include "lib/TestHarness.h"

#include "MyTestClass.h"

class MyFixtureSetup : public TestSetup

{

public:

void setup()

{

someValue = 2.0;

str = "Hello";

}

void teardown()

{}

protected:

float someValue;

std::string str;

MyTestClass myObject;

};

TESTWITHSETUP (MyFixture,Test1)

{

CHECK_DOUBLES_EQUAL (someValue, 2.0f);

someValue = 0;

// CppUnitLite doesn't handle system exceptions very well either

//myObject.UseBadPointer();

// A regular exception works nicely though myObject.ThrowException();

}

TESTWITHSETUP (MyFixture,Test2)

{

CHECK_DOUBLES_EQUAL (someValue, 2.0f);

CHECK_STRINGS_EQUAL (str, std::string("Hello"));

}

TESTWITHSETUP (MyFixture,Test3)

{

// Unfortunately, it looks like the framework creates 3 instances of MyTestClass

// right at the beginning instead of doing it on demand for each test. We would

// have to do it dynamically in the setup/teardown steps ourselves.

CHECK_LONGS_EQUAL (1, myObject.s_currentInstances);

CHECK_LONGS_EQUAL (3, myObject.s_instancesCreated);

CHECK_LONGS_EQUAL (1, myObject.s_maxSimultaneousInstances);

}

  • Хорошая обработка ошибок и крешей. Оригинальная версия CppUnitLite не обрабатывает их вовсе. Я добавил минимальную поддержку (только опциональные try|catch). Запуск тестов без поддержки исключений требует перекомпиляции тестов с включенной специальной директивой define, таким образом, это не так удобно, как использование аргументов командной строки, подобной той, которая реализована в Boost.Test.
  • Хорошая функциональность ASSERT. Это то место, где CppUnitLite показывает все свои недостатки. Макрос Assert самый плохой из всех рассмотренных фреймворков. Он не использует поток для вывода содержимого своих переменных, следовательно, необходим самодельный макрос для каждого типа объекта, который вы хотите использовать. Существует поддержка для double, long и строк, но все остальное вы должны реализовывать сами. Также, здесь нету никаких проверок, кроме как на равенство (или на сходимость, в случае чисел с плавающей запятой).
  • Поддержка различных методов вывода информации. Опять таки, имеется только один метод вывода, Но он хорошо изолирован, и достаточно просто добавить прочие методы.
  • Поддержка TestSuit'ов. Пожалуй, единственный фреймворк, который не поддерживает TestSuit. Мне они никогда не были нужны, но иногда, думаю, были бы весьма кстати.

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

NanoCppUnit

Я никогда не слышал ничего о NanoCppUnit пока Phlip не принес ее. Просматривая список возможностей, фреймворк казался содержит все что мне было нужно в CppUnitLite, к тому же не требовал никаких доработок и был готов к использованию.

Первый минус NanoCppUnit это ужасная поставка фреймворка. Если вы думаете, что у CppUnitLite дела плохи (как например отсутствие собственного сайта), то вы не совсем правы. По крайней мере, вы можете скачать zip-архив. В случае с NanoCppUnit вы должны вручную загрузить пять файлов по отдельности и поместить их в одну директорию. Я не шучу. Прям таки неимоверная забота о пользователях. Документация, найденая на сайте, скажем, не самая лучшая.

Так или иначе, я продолжил свои поиски простой программы с тестами и взялся за NanoCppUnit. Дистрибутив (согласно информации с веб-сайта) предназначен для использования только на Windows. Я решил что проблему будет просто исправить, но как оказалось изменения требуют, гораздо больше времени, чем я думал сначала (я прекратил это занятие когда получил ошибки использования макросов, где-то в недрах assert). В отличие от CppUnitLite, исходный код не так хорошо структурирован, полон повсюду ужасных макросов, делая совсем нетривиальным добавление новых возможностей, как, скажем, другие методы вывода информации. Пока я окончательно не закопался в ошибках, выглядело даже, что в фреймворке имеются примеры. Но в конце концов, я бросил затею портировать фреймворк по Linux, таким образм мои комментарии здесь базируются на догадках, которые у меня появились в процессе изучения исходного кода фреймворка.

  • Минимальное количество работы, требуемое для создания нового теста. Я думаю да. Я не уверен, что можно создать независимый тест, который является частью TestSuite, но как минимум создание TestSuite не требует ручной регистрации каждого теста. Это (возможно) самый простой из возможных тестов на NanoCppUnit.

struct MySuite: TestCase { };

TEST_(MySuite, MyTest)

{

float fnum = 2.00001f;

CPPUNIT_ASSERT_DOUBLES_EQUAL(fnum, 2.0f, 0.0001);

}

  • Простота в модификации и портировании. Не совсем. Зависимости Windows намного глубже, чем могло бы показаться вначале. Размер исходного кода – небольшой, но он достаточно запутанный, что заставляет чувствовать себя несчастным, работая с ним. Я уверен, что его можно портировать, нужно только приложить достаточно усилий, все же он не настолько большой.
  • Поддержка установщиков. Да, Установка и удаление осуществляются очень похожим образом, как это сделано в CppUnitLite.
  • Хорошая обработка ошибок и крешей. Понятия не имею, поскольку мне не удалось его запустить. В исходном коде встречаются консрукции try/catch, но непонятно, как и включить или выключить. Думаю, здесь ситуация не лучше, чем в CppUnitLite.
  • Поддержка различных методов вывода информации. Не совсем. Везде жестко забито использование потока, который отсылает сожержимое в OutputDebugString() в Windows. Я думаю, вывод текста по-умолчанию, имеет формат ошибок Visual Studio.
  • Хорошая функциональность ASSERT. Да. Хороший перечень конструкций Assert, включая приближение для чисел с плавающей запятой, больше чем, меньше чем и т.д.
  • Поддержка TestSuit'ов. Да. Хотя я не знаю, как это работает. Короче, не очень важно.
Одна из уникальных особенностей NanoCppUnit – это поддержка регулярных выражений, как части выражений Assert. Это очень необычно, но я могу себе представить, насколько это может быть удобным. Некоторое время назад, мне нужно было проверить определенную часть кода на соответствие некоторому формату, поэтому мне пришлось использовать sscanf а потом, проверять содержимое. С регулярными выражениями, там было бы все более элегантно. К сожалению, NanoCppUnit не дотягивает до уровня других фреймворков. Прямо сейчас, складывается такое ощущение, что над фреймворком кипит работа, доделывается много функциональности и еще не до конца структурирован исходный код.

Unit++

Продвигаясь дальше, сейчас мы рассмотрим фреймворк, который меньше всего похож на Xunit. Уникальность особенностей Unit++ заключается в том, что он больше соответствует C++ чем, CppUnit. Минутку, я правильно услышал? Больше соответствует C++? И это преподносится как достоинство? Возвращаясь назад, к моему идеальному фреймворку, можно отметить, что он вообще не имеет чего, что связывало бы его с С++. Однажды я начал размышлять об этом, и пришел к выводу, что нету вовсе никаких оснований полагать, что фреймворки для тестирования должны быть написаны на С++. Тесты которые вы пишете требуют того же языка, на котором написан код, который вы тестируете, но весь «оберточный» код – нет.
Эта точка зрения повлияла на мою оценку данного фреймворка.

Итак, так что же это значит – больше соответствует С++? Для начала – отсутствие макросов. Вы создаете TestSuite путем создания класса, отнаследованного от TestSuite. Это в принципе тот же самый подход, который реализован в большинстве предыдущих фреймворков, но только там это было скрыто.В действительности, это мне никак не помогает узнать, что же я делаю, и я бы не назвал это особенностью. Как результат, тесты более многословны, чем они могли бы быть.

Документация на среднем уровне. Она есть, но там нету детального описания и не перегружена исчерпывающими примерами.

  • Минимальное количество работы, требуемое для создания нового теста. Я боюсь по этому вопросу, оценка фреймворка будет просто удручающей. Фреймворк требует ручной регистрации тестов, и каждый тест должен быть частью TestSuite. Это делает создание нового теста нудным и провоцирует на появление ошибок (к примеру написать тест, но забыть его зарегистрировать). Я не знаю как вам, но для меня такой код не является локаничным и понятным, пока я не затрачу на его изучение некторое время. Как-то не очень оптимистично.

#define __UNITPP #include "unit++.h"

using namespace unitpp;

namespace

{

class Test : public suite

{

void test1()

{

float fnum = 2.00001f;

assert_eq("Checking floats", fnum, 2.0f);

}

public:

Test() : suite("MySuite")

{

add("test1", testcase(this, "Simplest test", &Test::test1));

suite::main().add("demo", this);

}

};

Test* theTest = new Test();

}

  • Простота в модификации и портировании. Средненько. Требуется STL и всякие соответствующие инструменты, как, скажем, iostream (который, к слову, имеет особые проблемы, при работае с STLPort). С другой стороны исходный код небольшой и самодостаточный, таким образом, возможность модификации и портирования существует, было бы время.
  • Поддержка установщиков. Еще один фреймворк, в котором я не увидел, как работать с установщиками. Как и в Boost.Test кажется, что использование конструкторов и деструкторов это все что вам нужно. Быстрый поиск в документации по поводу использования установщиков, их установки и удаления успехом не увенчались. Я не знаю, может я что-то упустил, или просто другие разработчики пишут код совсем по-другому. Я думаю, я мог бы создать класс, для каждого установщика, какого только захотел, поместил бы процедуру установки в конструктор а удаления в деструктор, и наследовал бы каждый новый клас от этого базового (и как то разобрался бы с тем, как создавать экземпляр этого класса и испольовать его для тестирования). Наверняка это возможно, но не совсем тривиально, так ведь? Опять таки, отсутствие установщиков не идет на пользу фреймворку.
  • Хорошая обработка ошибок и крешей. Средне. Фреймворк перехватывает регулярные исключения без крешей, но что с того. Нет поддержки системных исключений Linux. Нету возможности их отключить для отладки.
  • Поддержка различных методов вывода информации. Я не понял, как это реализовать, согласно документации. Возможно, есть способ с использованием поддерживаемой функциональности UI, но это неочевидно (и нету соответствующих примеров). Кроме того, имея в наличии уже проблемы по пунктам 1 и 3, у меня уже не было достаточно мотивации продолжать изучать фреймворк. Кстати, это один из нескольких фреймворков, у которых вывод по умолчанию происходит некорректно для таких IDE как Kdevelop.
  • Хорошая функциональность ASSERT. В фреймворке присутствует минимум возможностей по данному вопросу. Он предоставляет проверки на равенство и проверки по условиям, но это все. Даже нету возможности указать приближение для чисел с плавающей запятой. Хотя бы содержимое переменных он пишет правильно.
  • Поддержка TestSuit'ов. Да, как и большинство конкурентов.

Таким образом, Unit++ не является достойным вариантом. Отчасти, это потому, что у него отсутствуют такие особенности, которые интересуют меня, но он не предлагает ничего нового, чего нету в других фреймворках и у него куча собственных проблем. А то, что нету установщиков, я простить не могу.

CxxTest

После рассмотрения фреймворка, отличного от Xunit (Unit++) я без энтузиазма приступил к изучению самого эксцентричного из них всех, CxxTest. Я никогда до этого не слшал об этом фреймворке, но я знал, что он требует использования перла, для генерации некоторого кода на C++. Внутри меня что-то заскреблось.

Товарищи, я был неправ! Буквально через несколько минут использования CxxTest и беглого просмотра документации (пожалуй, одной из лучших), я точно осознал – это то что мне нужно. Это было для меня неожиданным сюрпризом, так как я уже был готов смириться с ситуацией и поделить победу между CppUnit и CppUnitLite.

Давайте начнем с самого начала. В чем заключается использование Perl и в чем отличие фреймворка от CppUnit? Ерез Волк (Erez Volk), автор CxxTest, обладал уникальной проницательностью, так как мы тестируем C++ программу, но мы не обязаны полагаться на C++ для всего на свете. Другие языки, такие как Java, лучше справляются с тем, что нам необходимо в плане юнит-тестирования, потому что там есть такое поянтие как reflection. С++ проигрывает в этом плане, поэтому мы должны идти на всевозможные трюки в C++, такие как ручная регистрация тестов, ужасные макросы и т.д. CxxTest обходит эти проблемы путем парсинга нашего простого теста и генерации кода, который вызывается в наших тестах. Результат просто превосходный. У нас есть вся гибкость, которая только нужна, при этом нет нужды использовать уродливые макросы, экзотические библиотеки или необычные особенности языка. По сути, требования CxxTest достаточно просты (ну понятно, что должна присутствовать возможность выполнять скрипты Perl).

Этап кодогенерации также просто интегрировать в регулярную build-систему. Замечательная документация описывает в деталях последовательность требуемых дейсвтий, требуемых для интеграции с make-файлами, проектными файлами Visual Studio или Cons. Один раз это проделав, вам больше не понадобится возвращатся к данному этапу еще раз.

Посмотрим, как обстоят дела в плане интересующих нас вопросов.

  • Минимальное количество работы, требуемое для создания нового теста. Очень хорошо. Это практически самый лучший результат. Если бы я придирался к мелочам, я бы сказал, что было бы еще лучше, если бы не было необходимости объявлять класс явно. Поскольку мы используем скрипт Perl, то нет никаких проблем исправить и это и еще больше приблизиться с моему идеальному фреймворку.

class SimplestTestSuite : public CxxTest::TestSuite

{

public: void testMyTest()

{

float fnum = 2.00001f;

TS_ASSERT_DELTA (fnum, 2.0f, 0.0001f);

}

};

  • Простота в модификации и портировании. CxxUnit требует самый простой набор возможностей языка (нету обработки исключений, нету шаблонных функций, и т.д.). Также не требуется никакой сторонней библиотеки. Фреймворк поставляется, как набор заголовочных файлов, таким образом, нет необходимости собирать отдельную библиотеку из исходников, или что-то вроде того. Функциональность хорошо разбита и разделена в оригинальном исходном коде, таким образом сделать модификацию также не составит труда.
  • Поддержка установщиков. CxxUnit получает наивысшую оценку в данной категории. Он не только поддерживает шаги установку/удаление на уровне отдельных тестов, но также поддерживает TestSuite на мировом (глобальном) уровне. Создание установщиков очевидно и требует лишь наследования от класса, и создания ровно столько функций, сколько вам нужно для данного теста. Что бы быть до конца последовательным, я бы влюбился в фреймворк, если бы это было сделано на предыдущем шаге, не говоря об упрощении кода, добавив еще установку/удаление в процессе генерации для каждого теста. Это позволило бы нам работать с объектами непосредственно на стеке, что позволило бы контролировать их время жизни корректно в каждом тесте. Да ладно, Нельзя иметь абсолютно все.

#include "MyTestClass.h"

class FixtureSuite : public CxxTest::TestSuite

{

public:

void setUp()

{

someValue = 2.0;

str = "Hello";

}

void tearDown() {}

void test1()

{

TS_ASSERT_DELTA (someValue, 2.0f, 0.0001f);

someValue = 13.0f;

// A regular exception works nicely though myObject.ThrowException();

}

void test2()

{

TS_ASSERT_DELTA (someValue, 2.0f, 0.0001f);

TS_ASSERT_EQUALS (str, std::string("Hello"));

}

void test3()

{

//myObject.UseBadPointer();

TS_ASSERT_EQUALS (1, myObject.s_currentInstances);

TS_ASSERT_EQUALS (3, myObject.s_instancesCreated);

TS_ASSERT_EQUALS (1, myObject.s_maxSimultaneousInstances);

}

float someValue;

std::string str;

MyTestClass myObject;

};

  • Хорошая обработка ошибок и крешей. Великолепная поддержка. Фреймворк отлавливает все исключения и выводит информацию о них, в таком формате, как и прочие ошибки (хотя поддержки системных ошибок Linux и здесь нету). Вы можете просто перезапустить тест с аргументом командной строки для скрипта Perl, что бы избежать перехватываение исключений и отлавливать их самостоятельно в отладчике. Фреймворк также дает вам возможность конфигурации макроса assert, позволяя вам при необходимости отлавливать исключения самостоятельно.
  • Поддержка различных методов вывода информации. Поддержка различных методов вывода осуществляется путем передачи параметра коммандной строки, который определяет, какой тип вывода использовать. Формат вывод по-умолчанию (error-printer) корректно обрабатывается IDE, и вы можете использовать некоторые другие (включая вывод с GUI, у которого есть индикатор прогресса, поддержка отчетов, или формат в стиле stdio). Добавление нового типа вывода также предусмотрен, что отражено в документации.
  • Хорошая функциональность ASSERT. Опять таки, наивысшая оценка. Здесь в наличие имеется полный набор assert функций, включающий обработку исключений, проверку утверждений и произвольные связи. Также имеется возможность выводить предупреждения, которые могут использоваться для определения разных частей кода, вызывающего один и тот же тест, или вывод TODO сообщений.
  • Поддержка TestSuit'ов. Да. Все тесты являются частью TestSuite.

Еще одной особенностью CxxUnit, с которой я не успел разобраться, это поддержка Mock объектов. Разработчики, использующие TDD знакомы со значением mock объектов, когда возникает необходимость тестировать взаимодействие между набором объектов. Несомненно, CxxUnit позволяет перегружать глобальные функции специфичными mock-функциями (предоставлен пример перегрузки fopen()). Я не думаю, что это полезно при работе с регулярными классами, для которых вы предоставлены сами себе.

А что же сделано не очень хорошо в CxxUnit? Немногое. К примеру, есть желание, что бы синтаксис тестов был более компактным, это касается больших проектов. Если вы будете следовать примерам в документации, то можете создать одиночный исполнитель (runner) для всех ваших тестов. Это может стать проблемой, если у вас сотни тестов, и вы сделаете небольшое изменение, это потребует перекомпиляции всего вашего кода.

После того как я пообщался с Ерезом и перепроверил документацию, я обнаружил, что есть полная поддержка этого в CxxUnit. По умолчанию, когда вы генерируете исполнитель тестов, он добавляет функцию main и несколько глобальных переменных, таким образом, линковка нескольких подобных исполнителей сопряжена с проблемами. Однако, это можно обойти, если вы будете генерировать исполнители с аргументом коммандной строки –part, это позволит не создавать main и глобавльные переменные для них. После этого вы можете слинковать их всех вместе и получить один модуль. Я бы хотел узнать, имеет ли смысл создание исполнителей для каждого TestSuite, или же было бы лучше объеденить их вместе. Стоит исследовать этот момент в будущем.

Выводы

После рассмотрения всех шести фреймворков, осталось четыре потенциальные кандидата: CppUnit, Boost.Test, модифицированный CppUnitLite и CxxTest.

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

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

Если вам необходима абсолютная простота, вы правильно поступите, если будете использовать CppUnitLite (или модифицированную версию), доработав ее в соответствии с вашими требованиями. Он хорошо структурирован, ультра-компактный, без сторонних зависимостей, таким образом, модификация будет совсем несложной. Главный минус – это минимум возможностей и примитивная функциональность assert.

Если вы собираетесь работать в основном на PC, не планируете делать каких либо модификаций фреймворка и не боитесь зависимостей от библиотек Boost, Boost.Test может оказаться самым удачным выбором.

Нужно ли создавать собственный фреймворк для юнит-тестирования? Я знаю, что Кент Бек рекомендует это в своей книге Test-Driven Development: By Example, и это может послужить хорошим опытом, но я не стану вам это рекомендовать. Возможно, может и есть смысл написать связанный список и организовать стек за короткое время, но я не буду рекомендовать создавать такое в промышленном коде, вместо использования STL. Я настоятельно рекомендую начать использовать один из трех фреймворков для тестирования, которые были упомянуты выше. Если вы готовы немного потрудиться, берите CppUnitLite.

Несмотря на то, что вы выберете, мы можете быть спокойны, используя один из этих трех фреймворков. Самая главная вещь, это то, что вы пишете юнит-тесты, или даже лучше, используете Test-Driven Development. Повторяя слова, Майкла Фивера, код без юнит тестов это legaсy код, вы же не хотите писать legaсy код?

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