Модульное тестирование и непрерывная интеграция при помощи Jenkins для C++ проектов

в 20:00, , рубрики: c++, Jenkins, netbeans, tdd, unit test, Программирование, метки: , , , ,

Думаю, что все знают, что такое модульные тесты, все знают или, по крайней мере, слышали, что такое непрерывная интеграция, и многие программируют на 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 проект

  1. Опционально. Я рекомендую создать новый репозитарий, если у Вас есть такая возможность. Принцип «один проект — один репозитарий» это очень хороший принцип, на мой взгляд.
  2. Получите локальную копию репозитария, который Вы используете. Для subversion это checkout, для git и mercurial это clone.
  3. Создайте в репозитарии папку для проекта (это в том случае, если у Вас не отдельный репозитарий). В моем случае это папка 'unittests' в svn репозитарии.
  4. Запустите Netbeans:
    image
  5. Создайте новый проект: меню File -> New Project.
  6. Выберите «C/C++ Application» в качестве типа проекта
  7. image
  8. Укажите имя проекта (у меня — 'helloworld'), и местоположение проекта (Project Location), в которой он будет находиться (у меня на скриншоте это та самая папка, которую я получил с subversion сервера. В поле 'Project Folder' Вы получите полный путь, где будет храниться полученный проект. Остальные поля можно оставить без изменения.
    image
  9. Нажмите на кнопку 'Finish'.
  10. После этого будет создан проект с единственным файлом исходного текста.
    image
  11. Обратите внимание, что в списке файлов файл 'main.cpp' помечен зеленым цветом. Этим цветом помечаются новые файлы, которые отсутствуют в репозитарии.
  12. Выберите в главном меню: Team -> Subversion -> Show Changes (можно щелкнуть правой кнопкой мыши по этому файлу и в попап меню выбрать Subversion -> Show Changes, но в дальнейшем я буду писать вариант через главное меню). В нижней части экрана появился список измененных файлов (пока это только main.cpp).
    image
  13. Можете щелкнуть правой кнопкой мыши по этому файлу и посмотреть, например, diff. Хотя пока это не интересно, так как в репозитарии еще было пусто.
    image
  14. Закоммитим изменения. Выберите в дереве проекта корень и в главном меню: Team -> Subversion -> Commit. Как видим здесь Netbeans создаст в репозитарии папку 'helloworld' и в ней файл main.cpp. А также все файлы проекта. Здесь же можно (нужно) добавить комментарий к комиту.
    image
  15. Обратите внимание, что после коммита в списке файлов main.cpp стал черный. То есть, в этом файле нет изменений.
  16. Хорошо бы еще попробовать собрать и запустить полученный проект. Нажимаем кнопку Play (или главное меню Run -> Run Project, или просто F6). Если все нормально, то после сборки Вы увидите сообщение типа: «RUN FINISHED; exit value 0; real time: 0ms; user: 0ms; system: 0ms»
  17. Если этого не произошло, то скорее всего есть какая-то проблема с настройками переменной среды окружения PATH или нехватка библиотек. Смотрите внимательно на сообщения, которые Вы получаете.
  18. Добавим в главную функцию вывод сообщения типа:
    cout << "Hello World!" << endl;
    

  19. И еще добавим подключение заголовочного файла:
    #include <iostream>
    

  20. Запустим еще раз, F6.
  21. Обратите внимание, что добавленные строки выделяются зеленой подсветкой слева от строки, а модифицированный файл в списке файлов теперь синий. На самом деле современные IDE это очень неплохие клиенты к системам версионного контроля. Эсили Вы поизучаете возможности, которые доступны в меню Team, то, возможно, что для некоторых из систем версионного контроля Вы можете отказаться от использования сторонних клиентов версионного контроля.
  22. Теперь diff файлов выглядит веселее.
    image
  23. Закоммитим и это изменение.

Сборка с командной строки

Для того, чтобы делать автоматическую сборку нам нужен какой нибудь сборочный скрипт для сборки с командной строки. Хорошим вариантом будет 'makefile' (хотя я знаю людей, которые C++ проекты предпочитают собирать при помощи 'ant'). Если Вы хорошо знаете как его создать, то сделайте это и этот параграф дальше не читайте. А если Вы новичок, то нужно что-то придумать.

Создание makefile отдельная большая тема, которая в рамки данной статьи никак не вмещается. Поэтому, мы пойдем самым простым для нас путем. Netbeans свой проект хранит, фактически, в виде makefile (правда, еще и с кучкой дополнительных файлов). Мы будем использовать, в качестве makefile именно его.

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

  1. Откройте терминал (cmd.exe или FAR для пользователей Windows).
  2. Перейдите в папку проекта ('/home/risik/work/unittests/helloworld' у меня).
  3. Выполните с командной строки команду 'make clean', а затем 'make'
  4. Если у Вас не было проблем при сборке проекта из Netbeans, то и здесь проблем возникнуть не должно.
  5. Именно эти файлы мы и закоммитили ранее вместе с файлами проекта. Но Вы, по каким то причинам этого не сделали, то самое время сделать это.
  6. Из Netbeans: правой кнопки мыши по проекту, и далее: Subversion -> Commit. Однако, кажется в Netbeans есть небольшая ошибка. Среди прочих файлов у меня в папке проекта есть файл с именем '.dep.inc', который нужен для корректной обработки зависимостей. Обойтись без него можно — сборка работает, но лучше его се же добавить вручную.
  7. Второй вариант — с командной строки или тем, что Вы обычно используете для работы с Вашей системой версионного контроля.

У меня получился такой список файлов:
корень проекта:

  • 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

  1. Откройте Jenkins в Вашем браузере (у меня на скриншотах он находится по адресу 192.168.1.109:8080/jenkins).
  2. Создайте новую задачу (Jenkins — New Job)
  3. Укажите имя для задачи и выберите тип задачи «Build a free-style software project».
    image
  4. Задайте конфигурацию, аналогично тому, что приведено на скриншотах.
    image
    image
  5. Рассмотрим подробнее поля, которые следует модифицировать.
    — Название работы. У меня она называется просто — 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'.
  6. Нажимайте кнопку сохранения и давайте попытаемся собрать проект.
  7. У меня он упал. Проблема легко локализуется, если посмотреть '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
    
    

  8. Ну здесь все просто. У меня сервере не оказалось команды 'make'.
  9. А после второй итерации я узнал, что у меня отсутствует g++. Ну это тоже лекго устранятся.
  10. Лишь на третий раз все оказалось удачно
    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. Для начала немного модифицируем наш проект. Чтобы в нем было хоть что-то, что можно тестировать, пусть строку приветствия формирует отдельный класс.

  1. Щелкните правой кнопкой мыши на 'Source Files' и из контекстного меню выберите пункт New -> C++ Class.
  2. В диалоге создания класса задайте имя нового класса, например, Helloer (уж простите меня за это название, но я не смог придумать как это назвать). Вообще правильно было бы здесь задать папку для нового файла. Ну хотя бы 'src'. Но я поленился.
    image
  3. В списке файлов появится пара файлов — Helloer.cpp и Helloer.h. В Netbeans они создаются в одной папке, а в других IDE они могут создаваться в двух раздельных папках, например, 'src' и 'include'.
  4. Как Вы можете заметить конструктор без параметров, конструктор копирования и виртуальный деструктор создаются автоматически.
  5. Сперва закоммитим то, что было сгененировано, а потом добавим то, что нам надо. image image image
  6. То есть, я добавил к классу Helloer конструктор со строкой, в качестве параметра, в которой будет указано кого именно будем приветствовать. А также добавил метод получения сообщения с приветствием.
  7. Закомитим изменения. Обратите внимание, что модифицировались также и файлы проекта, их тоже надо закоммитить.
  8. Откройте Helloer.h, щелкните правой кнопкой мыши внутри файла и выберите пункт Create Test -> Create CppUnit Test.
  9. В появившемся диалоге выберите элементы, которые будете тестировать. Я выбрал только метод message. image
  10. На следующем диалоге укажите имя теста. Кстати, если Netbeans не нашел нужной библиотеки (cppunit) он Вам об этом скажет здесь. У меня это пакет libcppunit-dev.
    image
  11. Вот что у меня получилось после генерации тестов. image
  12. В структуре проекта появилась ветка HelloWorldTests — именно это название я дал всей группе тестов в диалоге конфигурации теста. А в ней модуль прогона тестов данной группы (Runner), который у меня называется 'HelloWorldTestRunner.cpp' и пока единственный класс теста — HelloerTest (.cpp + .h).
  13. Попробуем запустить. Меню Run -> Test Project. иии… Я получил фэйл — модульные тесты не собрались. Так как не нашлось заголовочного файла 'Helloer.h'. Ну здесь просто — надо добавить путь к файлу. Для этого вызовите контекстное меню на группе тестов HelloWorldTests и выберите пункт Properties. Вы увидите диалог свойств папки проекта. image
  14. В этом диалоге в разделе 'C++ Compiler' найдите пункт 'Include Directories' и нажмиете кнопку '...' справа.
  15. В появившемся диалоге 'Include Directories' нажмите кнопку 'Add' image
  16. И наконец выберите папку, где находится Ваш заголовочный файл. Поскольку этот файл является частью прроекта, то хранить путь к файлу нужно в относительном виде, что и указано в правой части диалога. Поскольку я ранее поленился создавать даже папку 'src' когда создавал класс Helloer, то у меня этой папкой будет папка проекта. То есть, папка '.'. image
  17. Закрываем диалог свойств и пытаемся снова запустить модульный тест.
  18. На этот раз он собрался, запустился, но упал, на Assertion.image
  19. Впрочем, я сразу закоммитил, все что сгенерировалось. А вот теперь поправлю тест:
    image
  20. И изменения также закоммичу. Обратите внимание, что коммитить нужно не только исходные тексты, но и файлы проекта.
  21. Откройте файл 'HelloWorldTestRunner.cpp' если присмотреться, то его структура проста и понятна:
    — Подготавливаем прогон тестов.
    — Прогоняем их.
    — Печатаем результат.
  22. Netbeans сгенерировал печать результата в 'compiler compatible' формате. Это очень удобный формат для работы в Netbeans, но неудобный формат для Jenkins. Поэтому, я добавлю еще и печать в XML формате:
    ofstream xmlFileOut("cpptestresults.xml");
    XmlOutputter xmlOut(&result, xmlFileOut);
    xmlOut.write();
    
  23. Естественно нужно включить необходимые заголовные файлы:
    #include <cppunit/XmlOtputter.h>
    #include <ostream>
    
  24. Примечание: печать в XML формате, не вместо текстового, а вместе с текстовым.
  25. Имя файла («cpptestresults.xml» у меня) и путь к нему можно было выбрать другой. Но главное, что бы Вы знали какой. Эта информация нам скоро понадобится.

Настраиваем прогон автотестов в Jenkins

  1. Открыл браузер с Jenkins и вижу, что он уже сделал несколько сборок. Я ведь указал ему SCM сборку каждые 15 минут. А следовательно, если при очередной проверке, которая осуществляется каждые 15 минут, как нетрудно догадаться, были новые коммиты в репозитарий, то Jenkins собирал проект. Ну а поскольку я писал это все неторопливо, то поводы для пары прогонов у него нашлось :)
  2. Здесь есть важный нюанс. Если системное время сервера с Вашим репозитарием и системное время сервера с Jenkins расходятся, то можно получить очень странные эффекты. Например, Jenkins откажется признавать, что возможные модификации исходного текста в будущем. Поэтому настоятельно рекомендую настроить на всех серверах синхронизацию времени и лучше всего по какому-то общему серверу ntp. Да и Ваш десктоп хорошо бы сихронизировать аналогичным образом.
  3. Добавляем к джобе «Post build action», например так:
    image
  4. Пробуем собрать. и вуаля! Все собрано и тест пройден! И у Вас в билде появилась информация о пройденных тестах:
    image
  5. А после того, как соберет еще раз в джобе появится график. Ну пока он у меня такой:
    image
  6. Зато в 'Latest Tests Results' можно увидеть более подробную информацию о всех пройденных и провалившихся тестах (пока единственном):
    image
  7. Не смущайтесь, что в таблице общего списка тестов Вы видите (root). Jenkins заточен под Java. И здесь должны были быть имена пакетов.
  8. Давайте сделаем еще один тест. При этом, будем иметь в виду сразу небольшую модификацию функциональности: Если приветствовать некого (строка 'who' не задана), то и не надо говорить никаких приветов.
  9. То есть, мы должны добавить следующие строки к заголовочному файлу:
    image
  10. и такой метод к .cpp файлу:
    void HelloerTest::testMessageNobody() {
        Helloer helloer;
        string result = helloer.message();
        if (true /*check result*/) {
            CPPUNIT_ASSERT(result == "");
        }
    }
    
    
  11. коммитим. И теперь или ждем очередного запуска сборки по SCM или запускаем сборку вручную.
  12. Теперь мы видим, что у нас есть упавший тест!
    image
  13. Правда, сборка все равно помечена как успешная. Вернемся к этому вопросу чуть позже.
  14. Зато тренд стал симпатичнее:
    image
  15. Пофиксим образовавшийся test fail:
    string Helloer::message() const
    {
        if (who.length() == 0)
            return "";
        return (string)"Hello " + who;
    }
    

  16. И снова прогоним тесты в IDE. Теперь здесь все ОК. Закоммитим, соберем на стороне Jenkins и видим что там тоже теперь все хорошо (у меня успела пройти сборка со все еще поломанным тестом).
    image
  17. Несколько слов о статусе сборки. Jenkins позволяет настроить формирование статуса довольно гибко. Эту конфигурацию можно увидеть в настройке шага xUnit в джобе — пукты 'Failed Tests' и 'Skipped Tests'. Как будет лучше конкретно для Вас — я не знаю. Но я считаю, что каждый новый упавший тест должен приводить к поломке сборки, то есть, сборка должна помечаться, как красная. А вот те тесты, которые были поломаны ранее, должны приводить к «желтому статусу». Поэкспериментируйте!

Ну вот и все! Если все таки дошли до сюда по всем шагам, а не просто пролистнули на конец страницы, то Вы сделали первый шаг в автоматизации прогона модульных тестов в Вашем C++ проекте. И первый шаг к TDD. Удачи!

Ссылки

Автор: risik

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js