Думаю, что все знают, что такое модульные тесты, все знают или, по крайней мере, слышали, что такое непрерывная интеграция, и многие программируют на C++. Но я столкнулся с тем, что в интернете не так много информации о том, как же это все объединить и заставить работать вместе. Эта статья является попыткой дать новичками пошаговую инструкцию, которая позволит сделать первый шаг в создании модульных тестов для C++ проектов и организовать покоммитный прогон модульных тестов при помощи CI сервера.
Внимание: много букв и скриншотов, половина из которых избыточны. Особенно для тех кто уже в теме.
Предварительные требования
Для достижения поставленной задачи нам потребуется:
- Доступ к системе версионного контроля (subversion, mercurial или git). В данной статье я использую subversion репозитарий на своем собственном сервере. Вы можете использовать, например, любой свободный репозиторий типа GitHub или bitbucket.
- Сборочный сервер или сервер непрерывной интеграции (Continuous integration server). В данной статье используется Jenkins. Но есть и другие неплохие варианты. На данном сервере должна быть возможность сборки C++ проектов. Так же нужен плагин xUnit.
- Какая нибудь среда разработки на Вашем десктопе. В данной статье я использую Netbeans 7.3 С++ редакция.
- Компилятор C++. Я использую gcc.
Пара слов, почему именно Netbeans. Эта среда разработки единственная, которая включает и возможности по интеграции с системами версионного контроля и поддержку модульных тестов для C/C++ на уровне IDE. Это не означает, что другие IDE не могут быть использованы для создания модульных тестов, это означает, что в этой IDE начать разрабатывать модульные тесты может быть чуть проще, чем c другими IDE. То есть, проще для новичков.
Дабы меня не закидали помидорами поклонники Windows и Visual Studio, сразу оговорюсь, что Visual Studio в этом смысле, говорят, весьма хороша. Но я не пользуюсь Windows.
Создаем HelloWorld проект
- Опционально. Я рекомендую создать новый репозитарий, если у Вас есть такая возможность. Принцип «один проект — один репозитарий» это очень хороший принцип, на мой взгляд.
- Получите локальную копию репозитария, который Вы используете. Для subversion это checkout, для git и mercurial это clone.
- Создайте в репозитарии папку для проекта (это в том случае, если у Вас не отдельный репозитарий). В моем случае это папка 'unittests' в svn репозитарии.
- Запустите Netbeans:
- Создайте новый проект: меню File -> New Project.
- Выберите «C/C++ Application» в качестве типа проекта
- Укажите имя проекта (у меня — 'helloworld'), и местоположение проекта (Project Location), в которой он будет находиться (у меня на скриншоте это та самая папка, которую я получил с subversion сервера. В поле 'Project Folder' Вы получите полный путь, где будет храниться полученный проект. Остальные поля можно оставить без изменения.
- Нажмите на кнопку 'Finish'.
- После этого будет создан проект с единственным файлом исходного текста.
- Обратите внимание, что в списке файлов файл 'main.cpp' помечен зеленым цветом. Этим цветом помечаются новые файлы, которые отсутствуют в репозитарии.
- Выберите в главном меню: Team -> Subversion -> Show Changes (можно щелкнуть правой кнопкой мыши по этому файлу и в попап меню выбрать Subversion -> Show Changes, но в дальнейшем я буду писать вариант через главное меню). В нижней части экрана появился список измененных файлов (пока это только main.cpp).
- Можете щелкнуть правой кнопкой мыши по этому файлу и посмотреть, например, diff. Хотя пока это не интересно, так как в репозитарии еще было пусто.
- Закоммитим изменения. Выберите в дереве проекта корень и в главном меню: Team -> Subversion -> Commit. Как видим здесь Netbeans создаст в репозитарии папку 'helloworld' и в ней файл main.cpp. А также все файлы проекта. Здесь же можно (нужно) добавить комментарий к комиту.
- Обратите внимание, что после коммита в списке файлов main.cpp стал черный. То есть, в этом файле нет изменений.
- Хорошо бы еще попробовать собрать и запустить полученный проект. Нажимаем кнопку Play (или главное меню Run -> Run Project, или просто F6). Если все нормально, то после сборки Вы увидите сообщение типа: «RUN FINISHED; exit value 0; real time: 0ms; user: 0ms; system: 0ms»
- Если этого не произошло, то скорее всего есть какая-то проблема с настройками переменной среды окружения PATH или нехватка библиотек. Смотрите внимательно на сообщения, которые Вы получаете.
- Добавим в главную функцию вывод сообщения типа:
cout << "Hello World!" << endl;
- И еще добавим подключение заголовочного файла:
#include <iostream>
- Запустим еще раз, F6.
- Обратите внимание, что добавленные строки выделяются зеленой подсветкой слева от строки, а модифицированный файл в списке файлов теперь синий. На самом деле современные IDE это очень неплохие клиенты к системам версионного контроля. Эсили Вы поизучаете возможности, которые доступны в меню Team, то, возможно, что для некоторых из систем версионного контроля Вы можете отказаться от использования сторонних клиентов версионного контроля.
- Теперь diff файлов выглядит веселее.
- Закоммитим и это изменение.
Сборка с командной строки
Для того, чтобы делать автоматическую сборку нам нужен какой нибудь сборочный скрипт для сборки с командной строки. Хорошим вариантом будет 'makefile' (хотя я знаю людей, которые C++ проекты предпочитают собирать при помощи 'ant'). Если Вы хорошо знаете как его создать, то сделайте это и этот параграф дальше не читайте. А если Вы новичок, то нужно что-то придумать.
Создание makefile отдельная большая тема, которая в рамки данной статьи никак не вмещается. Поэтому, мы пойдем самым простым для нас путем. Netbeans свой проект хранит, фактически, в виде makefile (правда, еще и с кучкой дополнительных файлов). Мы будем использовать, в качестве makefile именно его.
Вообще говоря, вопрос о том, стоит ли в репозитарии хранить файлы проекта или нет это очень спорный вопрос. Я считаю, что нет. Но сборочный скрипт обязан быть в репозитари. Проект Netbeans в данном случае, это такой компромис. Так как, как я и сказал, файлы проекта Netbeans, фактически, являются сборочным скриптом. Не самым лучшим, но все же сборочным скриптом.
- Откройте терминал (cmd.exe или FAR для пользователей Windows).
- Перейдите в папку проекта ('/home/risik/work/unittests/helloworld' у меня).
- Выполните с командной строки команду 'make clean', а затем 'make'
- Если у Вас не было проблем при сборке проекта из Netbeans, то и здесь проблем возникнуть не должно.
- Именно эти файлы мы и закоммитили ранее вместе с файлами проекта. Но Вы, по каким то причинам этого не сделали, то самое время сделать это.
- Из Netbeans: правой кнопки мыши по проекту, и далее: Subversion -> Commit. Однако, кажется в Netbeans есть небольшая ошибка. Среди прочих файлов у меня в папке проекта есть файл с именем '.dep.inc', который нужен для корректной обработки зависимостей. Обойтись без него можно — сборка работает, но лучше его се же добавить вручную.
- Второй вариант — с командной строки или тем, что Вы обычно используете для работы с Вашей системой версионного контроля.
У меня получился такой список файлов:
корень проекта:
- Makefile
- .dep.inc
папка nbproject:
- Makefile-Debug.mk
- Makefile-Release.mk
- Makefile-impl.mk
- Makefile-variables.mk
- Package-Debug.bash
- Package-Release.bash
- configurations.xml
- project.xml
папка nbproject/private:
- Makefile-variables.mk
- configurations.xml
- private.xml
Начальная настройка Jenkins
- Откройте Jenkins в Вашем браузере (у меня на скриншотах он находится по адресу 192.168.1.109:8080/jenkins).
- Создайте новую задачу (Jenkins — New Job)
- Укажите имя для задачи и выберите тип задачи «Build a free-style software project».
- Задайте конфигурацию, аналогично тому, что приведено на скриншотах.
- Рассмотрим подробнее поля, которые следует модифицировать.
— Название работы. У меня она называется просто — helloworld-cpp
— В секции 'Source Code Management' укажите тип используемой системы версионного контроля. У меня это 'Subversion'.
— Дополнительные поля различаются для разным систем версионного контроля, для SVN самым важным из них является URL репозитария.
— Внимание: когда Вы будете задавать это урл, то скорее всего, Ваш Jenkins скажет что-то в духе «Не могу подключиться к репозитарию». В конце длинного красного трейс лога можно найти слова: '(Maybe you need to enter credential?)' и ссылку на ввод логина и пароля. Укажите свой логин и пароль на репозитарий, и Jenkins его запомнит. Другой вариант — указать логин и пароль прямо в урле. Или третий вариант — разместить аутентификационную информацию прямо в домашней папке пользователя, под которым работает Jenkins.
— В секции 'Build Triggers' я указал SCM сборку. То есть, сборку по расписанию. '*/15 * * * *' означает сборку каждый 15 минут.
— Примечание: вообще лучше бы вместо SCM использовать push сборку по триггеру от Вашей системы версионного контроля, но создание такой конфигурации выходит за рамки статьи.
— В секции 'Build' добавим новый шаг сборки типа 'Execute Shell'. Если у Вас Jenkins работает на Windows и у Вас на этом сервере не установлен cygwin (а как Вы вообще живете, если это так?), то, возможно, будет лучше выбрать вариант 'Execute Windows Batch Command'. - Нажимайте кнопку сохранения и давайте попытаемся собрать проект.
- У меня он упал. Проблема легко локализуется, если посмотреть 'Console Log':
Started by user anonymous Building in workspace /var/lib/jenkins/jobs/helloworld-cpp/workspace Checking out a fresh workspace because there's no workspace at /var/lib/jenkins/jobs/helloworld-cpp/workspace Cleaning local Directory . Checking out https://sergeyborisov.com/svn/teach/kcup_unittests/helloworld at revision '2013-02-25T01:37:54.054 +0700' A main.cpp A nbproject A nbproject/Makefile-Release.mk A nbproject/Makefile-impl.mk A nbproject/Package-Release.bash A nbproject/project.xml A nbproject/Makefile-Debug.mk A nbproject/Makefile-variables.mk A nbproject/configurations.xml A nbproject/private A nbproject/private/Makefile-variables.mk A nbproject/private/configurations.xml A nbproject/private/private.xml A nbproject/Package-Debug.bash A Makefile U . At revision 132 [workspace] $ /bin/sh -xe /tmp/hudson8376440745271858508.sh + make all /tmp/hudson8376440745271858508.sh: 2: /tmp/hudson8376440745271858508.sh: make: not found Build step 'Execute shell' marked build as failure Finished: FAILURE
- Ну здесь все просто. У меня сервере не оказалось команды 'make'.
- А после второй итерации я узнал, что у меня отсутствует g++. Ну это тоже лекго устранятся.
- Лишь на третий раз все оказалось удачно
Started by user anonymous Building in workspace /var/lib/jenkins/jobs/helloworld-cpp/workspace Updating https://sergeyborisov.com/svn/teach/kcup_unittests/helloworld at revision '2013-02-25T01:42:03.200 +0700' At revision 132 no change for https://sergeyborisov.com/svn/teach/kcup_unittests/helloworld since the previous build [workspace] $ /bin/sh -xe /tmp/hudson8326466894395366933.sh + make all for CONF in Debug Release ; do "make" -f nbproject/Makefile-${CONF}.mk QMAKE= SUBPROJECTS= .build-conf; done make[1]: Entering directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' "make" -f nbproject/Makefile-Debug.mk dist/Debug/GNU-Linux-x86/helloworld make[2]: Entering directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' mkdir -p build/Debug/GNU-Linux-x86 rm -f build/Debug/GNU-Linux-x86/main.o.d g++ -c -g -MMD -MP -MF build/Debug/GNU-Linux-x86/main.o.d -o build/Debug/GNU-Linux-x86/main.o main.cpp mkdir -p dist/Debug/GNU-Linux-x86 g++ -o dist/Debug/GNU-Linux-x86/helloworld build/Debug/GNU-Linux-x86/main.o make[2]: Leaving directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' make[1]: Leaving directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' make[1]: Entering directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' "make" -f nbproject/Makefile-Release.mk dist/Release/GNU-Linux-x86/helloworld make[2]: Entering directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' mkdir -p build/Release/GNU-Linux-x86 rm -f build/Release/GNU-Linux-x86/main.o.d g++ -c -O2 -MMD -MP -MF build/Release/GNU-Linux-x86/main.o.d -o build/Release/GNU-Linux-x86/main.o main.cpp mkdir -p dist/Release/GNU-Linux-x86 g++ -o dist/Release/GNU-Linux-x86/helloworld build/Release/GNU-Linux-x86/main.o make[2]: Leaving directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' make[1]: Leaving directory `/var/lib/jenkins/jobs/helloworld-cpp/workspace' Finished: SUCCESS
Создание тестов в Netbeans
Пока оставим Jenkins в покое и вернемся к Netbeans. Для начала немного модифицируем наш проект. Чтобы в нем было хоть что-то, что можно тестировать, пусть строку приветствия формирует отдельный класс.
- Щелкните правой кнопкой мыши на 'Source Files' и из контекстного меню выберите пункт New -> C++ Class.
- В диалоге создания класса задайте имя нового класса, например, Helloer (уж простите меня за это название, но я не смог придумать как это назвать). Вообще правильно было бы здесь задать папку для нового файла. Ну хотя бы 'src'. Но я поленился.
- В списке файлов появится пара файлов — Helloer.cpp и Helloer.h. В Netbeans они создаются в одной папке, а в других IDE они могут создаваться в двух раздельных папках, например, 'src' и 'include'.
- Как Вы можете заметить конструктор без параметров, конструктор копирования и виртуальный деструктор создаются автоматически.
- Сперва закоммитим то, что было сгененировано, а потом добавим то, что нам надо.
- То есть, я добавил к классу Helloer конструктор со строкой, в качестве параметра, в которой будет указано кого именно будем приветствовать. А также добавил метод получения сообщения с приветствием.
- Закомитим изменения. Обратите внимание, что модифицировались также и файлы проекта, их тоже надо закоммитить.
- Откройте Helloer.h, щелкните правой кнопкой мыши внутри файла и выберите пункт Create Test -> Create CppUnit Test.
- В появившемся диалоге выберите элементы, которые будете тестировать. Я выбрал только метод message.
- На следующем диалоге укажите имя теста. Кстати, если Netbeans не нашел нужной библиотеки (cppunit) он Вам об этом скажет здесь. У меня это пакет libcppunit-dev.
- Вот что у меня получилось после генерации тестов.
- В структуре проекта появилась ветка HelloWorldTests — именно это название я дал всей группе тестов в диалоге конфигурации теста. А в ней модуль прогона тестов данной группы (Runner), который у меня называется 'HelloWorldTestRunner.cpp' и пока единственный класс теста — HelloerTest (.cpp + .h).
- Попробуем запустить. Меню Run -> Test Project. иии… Я получил фэйл — модульные тесты не собрались. Так как не нашлось заголовочного файла 'Helloer.h'. Ну здесь просто — надо добавить путь к файлу. Для этого вызовите контекстное меню на группе тестов HelloWorldTests и выберите пункт Properties. Вы увидите диалог свойств папки проекта.
- В этом диалоге в разделе 'C++ Compiler' найдите пункт 'Include Directories' и нажмиете кнопку '...' справа.
- В появившемся диалоге 'Include Directories' нажмите кнопку 'Add'
- И наконец выберите папку, где находится Ваш заголовочный файл. Поскольку этот файл является частью прроекта, то хранить путь к файлу нужно в относительном виде, что и указано в правой части диалога. Поскольку я ранее поленился создавать даже папку 'src' когда создавал класс Helloer, то у меня этой папкой будет папка проекта. То есть, папка '.'.
- Закрываем диалог свойств и пытаемся снова запустить модульный тест.
- На этот раз он собрался, запустился, но упал, на Assertion.
- Впрочем, я сразу закоммитил, все что сгенерировалось. А вот теперь поправлю тест:
- И изменения также закоммичу. Обратите внимание, что коммитить нужно не только исходные тексты, но и файлы проекта.
- Откройте файл 'HelloWorldTestRunner.cpp' если присмотреться, то его структура проста и понятна:
— Подготавливаем прогон тестов.
— Прогоняем их.
— Печатаем результат. - Netbeans сгенерировал печать результата в 'compiler compatible' формате. Это очень удобный формат для работы в Netbeans, но неудобный формат для Jenkins. Поэтому, я добавлю еще и печать в XML формате:
ofstream xmlFileOut("cpptestresults.xml"); XmlOutputter xmlOut(&result, xmlFileOut); xmlOut.write();
- Естественно нужно включить необходимые заголовные файлы:
#include <cppunit/XmlOtputter.h> #include <ostream>
- Примечание: печать в XML формате, не вместо текстового, а вместе с текстовым.
- Имя файла («cpptestresults.xml» у меня) и путь к нему можно было выбрать другой. Но главное, что бы Вы знали какой. Эта информация нам скоро понадобится.
Настраиваем прогон автотестов в Jenkins
- Открыл браузер с Jenkins и вижу, что он уже сделал несколько сборок. Я ведь указал ему SCM сборку каждые 15 минут. А следовательно, если при очередной проверке, которая осуществляется каждые 15 минут, как нетрудно догадаться, были новые коммиты в репозитарий, то Jenkins собирал проект. Ну а поскольку я писал это все неторопливо, то поводы для пары прогонов у него нашлось :)
- Здесь есть важный нюанс. Если системное время сервера с Вашим репозитарием и системное время сервера с Jenkins расходятся, то можно получить очень странные эффекты. Например, Jenkins откажется признавать, что возможные модификации исходного текста в будущем. Поэтому настоятельно рекомендую настроить на всех серверах синхронизацию времени и лучше всего по какому-то общему серверу ntp. Да и Ваш десктоп хорошо бы сихронизировать аналогичным образом.
- Добавляем к джобе «Post build action», например так:
- Пробуем собрать. и вуаля! Все собрано и тест пройден! И у Вас в билде появилась информация о пройденных тестах:
- А после того, как соберет еще раз в джобе появится график. Ну пока он у меня такой:
- Зато в 'Latest Tests Results' можно увидеть более подробную информацию о всех пройденных и провалившихся тестах (пока единственном):
- Не смущайтесь, что в таблице общего списка тестов Вы видите (root). Jenkins заточен под Java. И здесь должны были быть имена пакетов.
- Давайте сделаем еще один тест. При этом, будем иметь в виду сразу небольшую модификацию функциональности: Если приветствовать некого (строка 'who' не задана), то и не надо говорить никаких приветов.
- То есть, мы должны добавить следующие строки к заголовочному файлу:
- и такой метод к .cpp файлу:
void HelloerTest::testMessageNobody() { Helloer helloer; string result = helloer.message(); if (true /*check result*/) { CPPUNIT_ASSERT(result == ""); } }
- коммитим. И теперь или ждем очередного запуска сборки по SCM или запускаем сборку вручную.
- Теперь мы видим, что у нас есть упавший тест!
- Правда, сборка все равно помечена как успешная. Вернемся к этому вопросу чуть позже.
- Зато тренд стал симпатичнее:
- Пофиксим образовавшийся test fail:
string Helloer::message() const { if (who.length() == 0) return ""; return (string)"Hello " + who; }
- И снова прогоним тесты в IDE. Теперь здесь все ОК. Закоммитим, соберем на стороне Jenkins и видим что там тоже теперь все хорошо (у меня успела пройти сборка со все еще поломанным тестом).
- Несколько слов о статусе сборки. Jenkins позволяет настроить формирование статуса довольно гибко. Эту конфигурацию можно увидеть в настройке шага xUnit в джобе — пукты 'Failed Tests' и 'Skipped Tests'. Как будет лучше конкретно для Вас — я не знаю. Но я считаю, что каждый новый упавший тест должен приводить к поломке сборки, то есть, сборка должна помечаться, как красная. А вот те тесты, которые были поломаны ранее, должны приводить к «желтому статусу». Поэкспериментируйте!
Ну вот и все! Если все таки дошли до сюда по всем шагам, а не просто пролистнули на конец страницы, то Вы сделали первый шаг в автоматизации прогона модульных тестов в Вашем C++ проекте. И первый шаг к TDD. Удачи!
Ссылки
- Using Hudson for C++/CMake/CppUnit: schneide.wordpress.com/2008/09/29/using-hudson-for-ccmakecppunit/ Здесь я подмотрел как сохранять результаты теста в XML
- How to Add Unit Tests to Existing C/C++ Projects in the Oracle Solaris Studio IDE. by Nik Krasilnikov. www.oracle.com/technetwork/articles/servers-storage-dev/howto-add-unittests-ide-1731716.html — Туториал по созданию модульных тестов в Netbeans.
- Adding Unit Tests to a C Project — NetBeans IDE Tutorial. Contributed by Susan Morgan. netbeans.org/kb/docs/cnd/c-unit-test.html — Еще один туториал по созданию модульных тестов в Netbeans.
- UnitTesting. wiki.codeblocks.org/index.php?title=UnitTesting — Туториал по созданию модульных тестов в code::blocks. Увы, в code::blocks все не так просто и прозрачно на старте, как в Netbeans.
Автор: risik