How to set up a PHP Symfony application test pipeline on Gitlab CI

E Ciotti
E Ciotti
Nov 1 · 5 min read

Gitlab offers tools to set up a CI pipeline in order to analyze and test your code automatically before merging. I’ll show an implementation for a PHP application (Symfony 3 and 4, or any other framework capable to be configured to run in test mode via an environment variable), with CI pipelines to analyze your code and run test with a dedicated database (dropped and recreated each time)

GitLab CI (taken from https://docs.gitlab.com/ee/ci/introduction/)

Libraries to install

I’m using PHPstan for static analysis, that also lint your scripts. Grab the latest stable phpstan.phar file and place it under bin directory on your project. PHPstan is an excellent tool to perform static analysis at different levels, spotting undefined variables and methods, inconsistent/missing PHPDoc params, wrong arguments and returning type hints.

wget https://github.com/phpstan/phpstan/releases/download/0.11.19/phpstan.phar -O bin/phpstan.phar

I also suggest installing the phar version of composer inside the bin directory, so that Gitlab and Docker won’t have to download it each time, making the CI pipeline and local building time faster.

wget https://getcomposer.org/download/1.9.0/composer.phar -O bin/composer.phar

And (of course) I’m using PHPUnit for unit and functional testing (database fixtures and lots of REST endpoint assertions). I personally prefer to install PHPUnit via composer in dev mode, so that the IDE can read the vendor directory and autocomplete the assertion methods.

composer require — dev phpunit/phpunit ^8

Scripts

You need a script to drop and recreate the database and then launch the test suite. Let’s place that into scripts/php_test_run.sh. My strategy is dropping, recreating, and run migrations so that I can also test the migrations will work properly when the code is released on production. As long as you merge migrations every once in a while, the few additional seconds are worthwhile (fixing a live prod migration gone wrong would take much more time + downtime). This is how you can do that in Symfony 3 and 4

php bin/console cache:clear --env=test
php bin/console doctrine:database:drop --force --env=test
php bin/console doctrine:database:create --env=test
php bin/console doctrine:migrations:migrate --env=test --no-interaction
php vendor/phpunit/phpunit/phpunit --testdox

Gitlab CI needs to know the container where you’ll launch your scripts. I’ll use a PHP 7.2 container where I’ll need to install some additional libraries. On this container, we’ll need everything needed to launch tests. In our case, we need to launch composer install (requiring zip packages) and your tests that will require the required project PHP extension. Very similar to the Dockerfile (if you are using docker), but it has to be a separate executable script in order for Gitlab to use it. Place this into scripts/scripts/ci-php-install.sh and customize

#!/bin/bash

# We need to install dependencies only for Docker
[[ ! -e /.dockerenv ]] && exit 0

set -xe

apt-get update -yqq
apt-get install git zlib1g-dev unzip -yqq

docker-php-ext-install pdo pdo_mysql zip # add other extension needed

Framework config

When executing tests, the framework has to run in a test mode. In Symfony 3/4, it’s set via the APP_ENV variable, automatically set at test execution time, so nothing to configure. What you need to do is only setting the database connection parameters to tell the app which database to use for testing. In Symfony 4 you can just set this in the .env.test file:

# .env.test
DATABASE_URL=mysql://myapptest:myapptest@127.0.0.1:3307/myapptest

If you use Symfony 3.2+, you can do the same with two files:

# app/config/config_test.yml
doctrine:
dbal:
driver: pdo_mysql
host: "%env(TEST_DATABASE_HOST)%"
port: "%env(TEST_DATABASE_PORT)%"
dbname: myapptest
user: myapptest
password: myapptest
# app/config/parameters.yml.dist (parameters.yml)
parameters:
env(TEST_DATABASE_PORT): 3307
env(TEST_DATABASE_HOST): 127.0.0.1

Note that I’m using a different port (3307) for the database, as I personally prefer to have a different running instance of the test database locally (very easy to set up, run and destroy with a script managing the containerized version). You can keep the same database port if you prefer, it’ll also make hte the Symfony 3 setup simpler.

Local execution

If you have done things correctly, launching the script scripts/php_test_run.sh should create the test database and run PHPUnit executing your Symfony controller tests. Read the Symfony doc if you need some assistance with that. If your database runs on docker or it’s not started, then I suggest you script it:

# docker-compose version with test db named 'db_test'
docker-compose up -d db_test && sh scripts/php_test_run.sh

Phpstan can be also launched locally:

phpstan analyse src 

Gitlab CI config

And finally, let’s see how you can launch phpstan and PHPUnit on Gitlab CI. You just need to place a file called .gitlab.ci.yml into the project root with the instructions and push the branch. Gitlab will automatically find it and execute it.

In this config, I’m basically telling Gitlab to pull a docker PHP image, along with a MySQL one (similar to a docker link). You can guess what the cache option does.

Variables section is the meaningful and tricky part: those variables are passed into the containers, and for my purposes, I need to pass the connection params (user, db, password) to the MySQL container and also pass the Symfony environment variable to the PHP container (running Symfony tests). Note that the database is aliased as mysql, therefore the php container (where you run tests) can only access it via the mysql network name (same behavior as in the docker-compose tool, if you are familiar with it):

default:
image: php:7.2-fpm
services:
- name: mysql:5.7
alias: mysql

cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/

variables:
MYSQL_ROOT_PASSWORD: pass_test
MYSQL_DATABASE: myapptest
MYSQL_USER: myapptest
MYSQL_PASSWORD: myapptest
# symfony 4
DATABASE_URL: 'mysql://myapptest:myapptest@mysql:3306/myapptest'
# symfony 3.2+
TEST_DATABASE_PORT: 3306
TEST_DATABASE_HOST: mysql
before_script:
- sh scripts/ci-php-install.sh
- php bin/composer.phar global require hirak/prestissimo
- php bin/composer.phar install -o

static_analysis:
stage: static_analysis
script:
- php bin/phpstan.phar analyse src --level=1

phpunit:
stage: test
script:
- sh scripts/php_test_run.sh

stages:
- static_analysis
- test

The part before_script (reference here) is launched before any other job on the default container (PHP in our case) with the code checked out and commands starting on the project root directory. In our case, we tell Gitlab to run our previously added scripts/ci-php-install.sh to install some Linux packages, then we run composer install to install the vendor libraries.

The stages part basically defines the pipeline to run in parallel.

The static analysis pipeline launches phpstan. The test pipeline launches PHPUnit. You can add more jobs for each pipeline of course.

After each push of your branch, both the branch and merge request GitLab pages will display a green/red icon based on the execution of the defined jobs:

Gitlab branch page

Clicking on the green (red if failing) icon will lead to more details:

Pipeline view

And clicking on the single jobs will print the full output from the scripts:

Job execution log

Thank for reading!

Clap if it was useful for you so that I’ll write more articles like this one.
Feel free to leave a comment for corrections or questions.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade