Программерские типсы и триксыОшибки есть в любых программах. В одних больше, в других меньше, но они есть. Для отлова багов все программисты тестируют свои творения, проверяя корректность работы кода в тех или иных условиях. Но чем больше и сложнее проект, тем труднее заниматься поиском ошибок и рефакторингом, поэтому было придумано модульное тестирование. В чем же состоит основная суть технологии юнит-тестирования? Ее реализация подразумевает под собой разбиение кода на изолированные части и тестирование каждой из них по отдельности. Конечно, подвергать проверке стоит только более-менее сложные куски кода твоей программы, писать тесты для функции умножения натурального числа на два не стоит. Такой подход позволяет программистам без страха вносить изменения в уже существующий код, не опасаясь его регрессии, то есть появления новых ошибок в уже оттестированных местах программы. Помимо этого можно добиться значительной экономии человекочасов на этапе тестирования кода, ведь каждый матерый кодер знает, что на отлов багов и всяческие проверки уходит до 80% времени, затраченного на проект в целом. Преимущества юнит-тестированияПомимо того, что unit-тесты дают программисту уверенность при рефакторинге кода и расширении его функциональности, есть еще ряд плюсов, о которых стоит рассказать. Прежде всего, это отделение интерфейса от реализации. Очень часто одни классы используют функции других. Это абсолютно нормально, но в модульном тестировании такое недопустимо. Если мы проверяем работу какого-либо класса, то эта проверка не должна распространяться на другие. Например, если тестируемый класс пользуется базой данных, то в unit-test мы должны абстрагироваться от нее, заменив БД заглушкой. Такой подход приводит к менее связному коду и минимизирует зависимости в системе, что является несомненным преимуществом — ошибка в одном месте программы не приводит к багам в другом. Также unit-тесты можно использовать как «живую» документацию к существующему коду. Проще говоря, в качестве примеров. Программисту, который в дальнейшем будет сопровождать наш код, не составит особого труда разобраться во всем хитроумном плане (к тому же, через полгода-год можно и самому забыть, как же эта штука должна работать и что с ней надо делать). Ну и наконец, подобные тесты помогают более четко представить задачи, стоящие перед кодером, приводят его мозг в тонус и помогают войти в рабочий ритм. Немного тонкостейВ идеальном случае юнит-тесты следует писать на этапе проектирования того или иного модуля. То есть, сначала мы определяем функциональность класса/модуля, затем пишем тесты под него и только потом основной код. Время на разработку в этом случае увеличивается, но зато значительно повышается эффективность. Это называется «разработка через тестирование». В среднем на одну строчку основного кода приходится три строки с тестовым. Некоторые подумают, что такие усилия себя не оправдывают, и возможно даже будут правы. Дело в том, что модульное тестирование стоит применять только в том случае, если оно снижает время на отладку, дает возможность поиска ошибок с меньшими затратами, нежели при других подходах, или обеспечивает дешевый поиск ошибок при изменениях кода в дальнейшем. Кроме этого, использовать юнит-тесты крайне желательно с какойнибудь системой контроля версий — например, SVN. В случае обнаружения проблем мы всегда сможем откатиться назад и начать все заново. Автоматизация в этом процессе крайне важна. Скажем, можно написать скрипты, которые каждую ночь будут загружать из хранилища последние версии исходников, компилировать их, проводить модульное тестирование и прочее. Наутро будет видно, насколько удачно прошел предыдущий день. Но эта статья не про технологию unit-тестирования. В сети достаточно материалов, чтобы подробно ознакомиться со всеми нюансами модульного отлова багов. Мы же сегодня рассмотрим готовые фреймворки для C++, которые служат основой для юнит-тестов. CppUnitНаверное, один из самых известных unit-test фреймворков для C++. Он основан на JUnit — библиотеке для модульного тестирования под Java. Несмотря на свою высокую популярность, CppUnit является достаточно сложной системой, и чтобы начать работать, придется прочитать изрядное количество документации. Использование CppUnit::TestCase
Вроде бы все просто, но если мы хотим выполнить много разных маленьких тестов для одного и того же набора данных, то нам понадобится класс TestFixture в связке с TestCaller. TestFixture позволяет задать набор начальных данных, на которых будут производиться тесты. Делается это с помощью переопределения функции-члена setUp(). В случае, если нам понадобится убрать за собой (например, освободив выделенную память), можно воспользоваться методом tearDown(), в коде которого нам нужно будет выполнить необходимые действия. Для самих тестов следует написать свои методы. Их может быть сколько угодно, и названия этих функций произвольны. Использование CppUnit::TestFixture
Чтобы запустить тесты на выполнение, нам понадобится класс TestCaller. Это шаблонный класс, созданный в примере выше, который использует наследников TestFixture. Конструктор TestCaller принимает в качестве аргументов два параметра, один из которых — это имя теста, а второй — указатель на метод, выполняющий непосредственные проверки. Для наглядности немного кода: Использование CppUnit::TestCaller
То, что мы сейчас сделали, в терминологии CppUnit называется Test Case. Использовать Test Case в качестве основного механизма вызова тестов — не очень хорошее решение. Во-первых, мы не увидим никакой информации о том, как протекает тестирование, а во-вторых, TestCaller работает только с одним из тестов. Но тест-кейсы можно объединить в Suite с помощью класса CppUnit::TestSuite. Для этого у него имеется специальный метод addTest, принимающий в качестве параметра указатель на объект типа TestCaller. Использование CppUnit::TestSuite
В свою очередь, запустить все наборы тестов (Test Suite) поможет класс TestRunner. С помощью метода addTest мы добавляем в ранер нужные нам сьюты. Но добавляем не просто так, а с помощью статического метода suite, который возвращает указатель на объект TestSuite. Использование CppUnit::TestRunner
Вот такая краткая инструкция по использованию этого фреймворка. В документации к нему есть еще множество нюансов, которые помогут максимально подстроить тесты под любые нужды. Но, как я уже сказал выше, придется много читать. Google C++ Testing FrameworkЕще один популярный фреймворк для модульного тестирования — Google C++ Testing Framework. Гугл выложил его в публичный доступ под BSD-лицензией в середине 2008 года, и с тех пор он достаточно быстро набрал армию поклонников. Google Test (будем называть его так для краткости) основан на методологии xUnit, что делает его очень похожим на CppUnit. Но, в отличие от последнего, гугловские тесты проще в использовании, а потому позволяют сосредоточиться на разработке кода тестовых функций, а не инфраструктуры для их использования. Как и в CppUnit, минимальной единицей тестирования является одиночный тест. Каждый тест основан на утверждении в виде макроса — например, ASSERT_TRUE или EXPECT_GE. Утверждения бывают фатальные и не фатальные. Фатальные макросы вида ASSERT_xxx приводят к остановке процесса тестирования, а нефатальные (или мягкие) утверждения вида EXPECT_xxx позволяют довести тесты до конца, просто выводя информацию об ошибке. Для создания одного элементарного теста нам понадобится макрос TEST. Первым его параметром является имя набора теста, а вторым — имя теста. Тесты, схожие по смыслу, должны группироваться в наборы. Для примера взглянем на код: Использование макроса TEST()
В Google Test также имеются и fixture, то есть классы для использования единой конфигурации в нескольких тестах. Такие классы являются потомками ::testing::Test, у которого имеются методы SetUp и TearDown для инициализации и освобождения ресурсов соответственно. Это очень похоже на то, что мы делали в TestFixture в CppUnit. Использование макроса ::testing::Test
Для создания теста, использующего класс набора данных, нам пригодится макрос TEST_F(). Аргументы, передаваемые TEST_F, аналогичны тем, что используются в TEST(), за единственным исключением — имя набора теста должно совпадать с именем тестового класса. Главное тут не перепутать TEST с TEST_F, иначе мы получим ошибки на этапе компиляции. Использование макроса TEST_F()
Ну и, наконец, для запуска всех описанных тестов можно воспользоваться макросом RUN_ALL_TESTS(). Простота Google Test заключается в том, что не надо никаких дополнительных движений по добавлению функций в какие-либо контейнеры и прочее. Все, что описано с помощью макросов, выполнится при вызове RUN_ALL_TESTS. Использование макроса RUN_ALL_TESTS()
Самые ленивые могут не писать свою собственную функцию main, а воспользоваться готовой из гугловского набора. Для этого достаточно лишь прилинковать библиотеку gtest_main. ЗаключениеНабор фреймворков для юнит-тестов не ограничивается приведенным мини-списком. Есть еще Boost Test, CxxTest, API Sanity Autotest для динамических C/C++ библиотек в Unix-подобных ОС и множество других. Описать все их особенности в одной статье просто невозможно. Главное, что у того, кого по-настоящему заинтересует Unit Tests или разработка через тестирование, всегда будет нормальный выбор. Ссылки
|