О юнит-тестах в C++

Привет всем!

Сегодня я расскажу о средах для написания юнит-тестов в C++, а также, в частности, о том, как мы используем юнит-тесты в PDF Creator’e.

В определенный момент развития PDF Creator’a стало ясно, что без удобной процедуры тестирования становится все тяжелее и тяжелее. Значительные объемы кода (не везде отрефакторенного), множество клиентов, определенные тонкости формата PDF и работы библиотеки – эти факторы придают особый вес каждому изменению в коде. Сейчас как минимум при выпуске каждой версии (в действительности чаще) мы проверяем набор “визуальных” тестов библиотеки (на выходе получается PDF, нужно глазами оценить правильность результата), затем конвертацию emf-файлов (тоже визуально), ASP- и vbs-скрипты. Множество этих тестов постоянно пополняется, но, к сожалению, правильность получаемых результатов далеко не гарантирует того, что в ходе работы над версией не было ничего нарушено…

Поэтому при работе над PDF Creator 3.9 мы много рефакторили код, одна из задач рефакторинга – подготовить его для тестов. После этого встал вопрос – какую среду использовать. Изначально варианта было два – классический CppUnit (http://sourceforge.net/projects/cppunit/) и Boost Test Library (http://www.boost.org/libs/test/doc/index.html)

Немного о требованиях, которые мы предъявляли к выбору среды. 1) Минимум подготовительных действий для написании теста 2) Удобный обзор результата, т.е. какие тесты не прошли, где ошибка и т.п. Интеграция в Visual Studio приветствуется 3) Кросс-платформенность (в будущем PDF-библиотека планируется платформо-независимой) 4) Компактность самой среды 5) Юнит-тесты не должны попадать в Release Build.

CppUnit я раньше использовал и, в целом, не был в восторге. Нарекания как минимум по пунктам 1, 2, 4. Много лишнего и ненужного конкретно нам. Нетривиальный интерфейс, плюс кое-что дописывать приходится. В общем воспоминания (особенно после NUnit) негативные.

Собрались использовать Boost.Test, но, как выяснилось, для этого необходимо тащить в проект полностью Boost Library, со всеми нужными и ненужными примочками. Я не большой знаток Boost, если есть способ это обойти – напишите, пожалуйста. В общем, от Boost также отказались.

После этого озадачились поиском альтернативы. http://www.gamesfromwithin.com/articles/0412/000061.html – на мой взгляд, отличная статья, сравнивающая различные среды тестирования. Must read!

После прочтения заинтересовала, разумеется, CxxTest (http://cxxtest.sourceforge.net/) Впечатления – так себе, документация обширная, но не особо прозрачная. Последнее обновление датировано 2004-м годом. Для компиляции нужен Perl(!), в общем – снова “мимо кассы”.

После этого пробовали еще несколько сторонних вариантом, но ничего путного не нашли. В тот момент, когда надежда была уже почти потеряна :), натолкнулись на UnitTest++ (гром фанфар и аплодисменты) : http://unittest-cpp.sourceforge.net/. Один из авторов – Noel Llopis, он же автор замечательной статьи, ссылка на которую чуть выше. Вот и его описание этой среды: http://www.gamesfromwithin.com/articles/0603/000108.html

Эта среда полностью удовлетворила наши 5 пунктов требований. Написание теста – тривиальная задача. Среда легкая (и заявлена как кросс-платформенная). Непопадание тестов в Release решается простым выделением тестов в отдельный проект. Среда прекрасно интегрируется в Visual Studio, при запуске тестов в окне Output выводится количество тестов, время прогона, обзор ошибочных тестов. В общем – не жизнь, а сказка 🙂

Мы начали использовать эту среду в PDF Creator. Сразу стало ясно, что все эти “вкусности” достались не “за даром”. Заглянем под капот этой среды; вот так выглядит типичный тест:

TEST(SomeTest) {const int expected = 123; int res = testedFunction(); CHECK_EQUAL(res, expected); }

При компиляции макрос TEST разворачивается в класс с именем TestSomeTest, который является наследником некоего UnitTest::Test. Последней строчкой кода после этого развертывания будет

void TestSomeTest::RunImpl(UnitTest::TestResults& testResults_) const

Т.е тело теста формирует метод RunImpl. Рассмотрев макрос CHECK_EQUAL, заметим, что он использует аргумент testResults_, и, следовательно, может корректно использоваться только в определенных местах (методах, определенных через тестовые макросы). А значит, о нормальном рефакторинге можно забыть (в частности, не получится выделить тестовый метод).

Другой момент – нельзя пометить метод временно не выполняемым, ибо макросы есть макросы. Решение – только полностью закомментировать тестовый метод. Еще минус – средства IntelliSense при написании тестового метода работают не всегда адекватно, снова тлетворное влияние макросов. Ну и как следствие макросов – сложно нормально расширять/улучшать функциональность среды. Видимо, поэтому она и давненько не обновлялась (с апреля 2007).

А в общем – неплохо. По крайней мере, проблему с рефакторингом можно более-менее обойти, а с остальным вполне можно мириться.

В следующей своей статье планирую описать подробней некоторые тонкости тестирования с UnitTest++, а также обязательно опишу процесс поиска утечек памяти с memleaks.

Виталий Шибаев Разработчик PDF Creator

3 thoughts on “О юнит-тестах в C++”

  1. Спасибо.
    Познавательная статья, явна требующая продолжения 🙂

    Пошел читать про UnitTest++.

  2. Все это старье, попробуйте google test suite. Это на первом месте. На втором однозначно boost – ничего там тащить в проект не надо, достаточно подключить *.h файл.

  3. Вы пишете на C++, не используя Boost ?!?
    Кстати, от CppUnit я тоже отказался – слишком много лишней возни со сборкой,
    подключением lib-ов и всех этих testrunner-ов, да и код далеко не самый простой.
    Взгляните:

    ///////////////
    /// CppUnit ///
    ///////////////

    int main(int argc, char* argv[])
    {
    CppUnit::Test *suite = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    CppUnit::TextUi::TestRunner runner;
    runner.addTest( suite );
    runner.setOutputter( new CppUnit::CompilerOutputter( &runner.result(), std::cerr ) );
    bool wasSucessful = runner.run();
    return wasSucessful ? 0 : 1;
    }

    //////////////////
    /// Boost.test ///
    //////////////////

    BOOST_AUTO_TEST_CASE(mul_test_case)
    {
    BOOST_CHECK(mul(2 * 2) == 4);
    }

Leave a Reply

Your email address will not be published. Required fields are marked *