Задача – запускать PHPUnit для тестов кода бекенда.
Сам PHPUnit будет запускаться из Codeception.
Задача в Jenkins должна триггериться из Github, при создании Pull Request – используем Github Pull-Request Builder плагин.
Для просмотра отчётов о тестах – используем Allure.
Jenkins запущен в Docker-контейнере, и все процессы будет запускать в контейнерах.
Содержание
Проверка тестов
Что бы получить представление о том, как и что будет запускаться, и заодно проверить, что всё работает, как ожидается – сначала выполняем последовательный запуск на локальной машине.
PHP:
[simterm]
$ docker run -ti php:7.2 php --version PHP 7.2.19 (cli) (built: Jun 1 2019 00:50:51) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
[/simterm]
Выполняем init проекта:
[simterm]
$ docker run -ti -v $(pwd):/data/ php:7.2 /data/init --env=Development Yii Application Initialization Tool v1.0 ... ... initialization completed.
[/simterm]
Теперь – Composer, что бы установить все зависимости:
[simterm]
$ docker run -ti -v $(pwd):/data/ --workdir /data composer install --optimize-autoloader --ignore-platform-reqs Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Package operations: 118 installs, 0 updates, 0 removals ... Generating optimized autoload files
[/simterm]
И теперь сами юнит-тесты, для начала из каталога backend
:
[simterm]
$ docker docker run -ti -v $(pwd):/data/ codeception/codeception run -c /data/backend/ unit Codeception PHP Testing Framework v3.0.1 Powered by PHPUnit 7.5.9 by Sebastian Bergmann and contributors. Running with seed: Backend\tests.unit Tests (0) ---- Time: 99 ms, Memory: 16.00 MB No tests executed!
[/simterm]
Окей – тестов ещё нет, девелоперы допилят их потом, наша задача – make it works. Переходим к Jenkins.
Jenkins
Создаём Pipeline-джобу, настраиваем GitHub project:
Пишем первый, тестовый запуск:
node { stage('Clone repo') { git branch: "master", url: "[email protected]:orgname/reponame.git", credentialsId: "jenkins-github" } stage('Build app') { docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "php init --env=Development" } } }
Запускаем:
ОК, работает – добавляем вызов Composer:
node { stage('Clone repo') { git branch: "master", url: "[email protected]:orgname/reponame.git", credentialsId: "jenkins-github" } stage('Build app') { docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "php init --env=Development" } docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "composer install --optimize-autoloader --ignore-platform-reqs" } } }
Запускаем, и:
…
– Installing orgname/itunes-receipt-validator (dev-master c8c11ba): Cloning c8c11ba7ba
Failed to download orgname/itunes-receipt-validator from source: Failed to execute git clone –no-checkout ‘[email protected]:orgname/itunes-receipt-validator.git’ ‘/var/lib/jenkins/workspace/BackendUnitTests/ProjectName/vendor/orgname/itunes-receipt-validator’ && cd ‘/var/lib/jenkins/workspace/BackendUnitTests/ProjectName/vendor/orgname/itunes-receipt-validator’ && git remote add composer ‘[email protected]:orgname/itunes-receipt-validator.git’ && git fetch composerNow trying to download from dist
– Installing orgname/itunes-receipt-validator (dev-master c8c11ba): Downloading (connecting…)… Downloading (failed)[Composer\Downloader\TransportException]
The “https://api.github.com/repos/orgname/itunes-receipt-validator/zipball/c8c11ba7ba1741c7dec248e6f9c0787cb146730a” file could not be downloaded (HTTP/1.1 404 Not Found)
…
Добавим Github Auth token:
... docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "composer config -g github-oauth.github.com 64b***554" sh "composer install --optimize-autoloader --ignore-platform-reqs" } ...
Повторяем:
Работает.
Добавляем вызов самих тестов:
node { stage('Clone repo') { git branch: "master", url: "[email protected]:orgname/reponame.git", credentialsId: "jenkins-github" } stage('Build app') { docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "php init --env=Development" } docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "composer config -g github-oauth.github.com 64b***554" sh "composer install --optimize-autoloader --ignore-platform-reqs" } } stage('Backend tests') { docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "codeception run -c backend/ unit" } } }
Jenkins Docker Plugin – The container started but didn’t run the expected command. Please double check your ENTRYPOINT.
Следующая ошибка – при запуске контейнера с codeception
:
…
[Pipeline] withDockerContainer
Jenkins seems to be running inside container 81f6a8cbdb28a86b5e156a929ef06c2a68dd5c716910f0ab66073656c32c6472
$ docker run -t -d -u 0:0 -v /var/run/docker.sock:/var/run/docker.sock -w /var/lib/jenkins/workspace/BackendUnitTests/ProjectName –volumes-from 81f6a8cbdb28a86b5e156a929ef06c2a68dd5c716910f0ab66073656c32c6472 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** codeception/codeception cat
$ docker top 7e909bf25e232c951658478c1a82a93504061a4de4d90b2134680b3ca5f59f36 -eo pid,comm
ERROR: The container started but didn’t run the expected command. Please double check your ENTRYPOINT does execute the command passed as docker run argument, as required by official docker images (see https://github.com/docker-library/official-images#consistency for entrypoint consistency requirements).
Alternatively you can force image entrypoint to be disabled by adding option `–entrypoint=”`.
…
process apparently never started in /var/lib/jenkins/workspace/BackendUnitTests/ProjectName@tmp/durable-49ccb7e5
…
[Pipeline] End of Pipeline
ERROR: script returned exit code -2
Finished: FAILURE
Уже не раз встречался с такой проблемой при использовании некоторых Docker образов в Jenkins.
Окей.
Обычно просто собираю свой образ из Alpine или Debian/Ubuntu с нужным сервисом, но в этот раз было совсем лень, поэтому нашёл “решение”.
В Jenkins issue, открытой, внезапно, ещё в 2016, и в которую до сих пор комментируют, предлагается задать --entrypoint=""
.
Но тогда не будет работать вызов sh "codeception run [...]"
.
Значит – надо найти исполняемый файл в Docker-образе Codeception. Можно было бы поискать Dockerfile
образа и проверить его entrypoint
, или запустить контейнер, и сделать docker inspect
, но нашёл иначе, хотя и “костыльно”.
Запускаем контейнер локально, переопределяем entrypoint
в bash
:
[simterm]
$ docker run -ti --entrypoint bash codeception/codeception root@596960d14192:/project#
[/simterm]
Попытался найти исполняемый файл codeception
:
[simterm]
root@596960d14192:/project# find / -name codeception /root/.composer/cache/files/codeception /repo/vendor/codeception
[/simterm]
Но это всё директории…
А как он вызывается?
Начал набирать code, кликнул TAB, и подставился сам исполняемый фал – им оказался codecept
, а не codeception
.
Обновляем вызов в пайплайне – добавляем --entrypoint=""
и меняем вызов в sh
:
... stage('Backend tests') { docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') { sh "/repo/codecept run -c backend/ unit" } } ...
Запускаем:
Хорошо.
Пока дописал этот пост до этого места – девелоперы добавили “тестовый тест”, запускаем с ним, плюс добавляем создание репортов в XML и HTML, см. документацию тут>>>.
... stage('Backend tests') { docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') { sh "/repo/codecept run -c backend/ unit --coverage-xml --coverage-html" } } ...
Запускаем:
Allure reports
Далее – хочется добавить вывод репортов в Jenkins.
У нас уже есть Allure Reports Plugin для других билдов – попробуем прикрутить его к Codeception/PHPUnit.
Другой вариант – использовать Clover PHP Plugin.
Документация по Allure для Codeception – тут>>>.
Правим composer.json
, добавляем установку Allure:
... { "require": { ... "allure-framework/allure-codeception": ">=1.1.0" } } ...
Обновляем конфиг backend/codeception.yml
, добавляем extensions
:
namespace: backend\tests actor: Tester paths: tests: tests log: tests/_output data: tests/_data helpers: tests/_support settings: bootstrap: _bootstrap.php colors: true memory_limit: 1024M modules: config: Yii2: configFile: 'config/test-local.php' extensions: enabled: - Yandex\Allure\Codeception\AllureCodeception config: Yandex\Allure\Codeception\AllureCodeception: deletePreviousResults: false outputDirectory: allure-results ignoredAnnotations: - env - dataprovider
В джобе добавляем обновление composer.lock
(но лучше его перегенерить, и обновить в репозитории, что не выполнять update
каждый раз во время запуска тестов, потом так и сделаю):
... stage('Build app') { ... sh "composer update --lock --ignore-platform-reqs" sh "composer install --optimize-autoloader --ignore-platform-reqs" } } ...
Добавляем сам Allure плагин и сбор репортов:
... stage('Backend tests') { docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') { sh "/repo/codecept run -c backend/ unit" } } stage('Reports') { allure([ includeProperties: false, jdk: '', properties: [], reportBuildPolicy: 'ALWAYS', results: [[path: 'backend/tests/_output/allure-results']] ]) } ...
Запускаем билд:
И получаем репорт:
Github webhook
Последним шагом – надо настроить Github репозиторий, что бы он выполнял webhook при создании Pull Request к мастер-бранчу (в нашем случае develop-бранчу) и запускал юнит-тесты.
См. Jenkins: Github Pull-Request Builder плагин.
В настройки джобы добавляем GitHub project:
Создаём токен в Github, настраиваем плагин.
В Build Triggers включаем GitHub Pull Request Builder, отмечаем Use github hooks for build triggering.
В List of organizations. Their members will be whitelisted добавляем организацию Github (или отдельного юзера в Admin list, если нет организации в Github), и отмечаем Allow members of whitelisted organizations as admins:
Если вебхук не создался сам – добавляем вручную:
в Pyaload URL указываем https://<jenkins-URL>/ghprbhook/, указываем Let me select individual events, и выбираем события, при которых вебхук будет срабатывать – Issue comments и Pull requests:
Создаём PR:
Проверяем статус вебхука – должен был выполнить запрос к Jenkins-у:
Билд в Jenkins:
И PR прошёл:
В целом – на этом всё.
Далее – наводим марафет: выносим Jenkinsfile в репозиторий, всякие репозитории-бранчи в нём – в параметры Jenkins-джобы.
Финальный билд-скрипт выглядит так:
node { stage('Clone repo') { git branch: "${APPLICATION_BRANCH}", url: "${APPLICATION_URL}", credentialsId: "jenkins-github" } stage('Build app') { docker.image('php:7.2').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "php init --env=Development" } docker.image('composer').inside('-v /var/run/docker.sock:/var/run/docker.sock') { sh "composer config -g github-oauth.github.com ${GITHUB_TOKEN}" sh "composer update --lock --ignore-platform-reqs" sh "composer install --optimize-autoloader --ignore-platform-reqs" } } try { stage('Backend tests') { docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') { sh "/repo/codecept run -c backend/ unit" } } stage('Frontend tests') { docker.image('codeception/codeception').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') { sh "/repo/codecept run -c frontend/ unit" } } } finally { stage('Reports') { allure([ includeProperties: false, jdk: '', properties: [], reportBuildPolicy: 'ALWAYS', results: [[path: 'backend/tests/_output/allure-results'],[path: 'frontend/tests/_output/allure-results']] ]) } } }
Тут кроме бекенда добавлены тесты фронтенда, а вызов тестов завёрнут в try/catch
, что бы всегда выполнять сбор репортов Allure (в finally
), иначе, если тесты остановятся с FAILED – то до stage("Reports')
выполнение скрипта не дойдёт, и сфейленные тесты не попадут в Allure.
Вызов скрипта в Jenkins:
И репорты – в backend один тест прошёл, во frontend один упал:
Готово.