DEV Community

Cover image for Test automation for Laravel 7 and MySQL with GitHub Actions
Roberto B.
Roberto B.

Posted on • Updated on

Test automation for Laravel 7 and MySQL with GitHub Actions

GitHub Actions (https://github.com/features/actions) is a powerful service provided by GitHub for continuous integration and continuous delivery.

GitHub Actions allows you to execute some commands when a GitHub event is triggered. For example you can automate the execution of unit tests on your code base when you push your code in the repository.

With GitHub Actions you can:

  • setup your runner or container (with your stack: database, compilers, executables, web server...);
  • fetch your code from the repository and store in you running container;
  • setup your additional services like MySQL database;
  • configure your application (configure the database connection, generate all needed keys, warmup the cache, fix permissions);
  • execute some command (for example the test suite via phpunit).

The scenario that I would like to explain you is:

I have a Web App based on Laravel 7, every time I push some new code on develop branch on the GitHub repository, I would like to execute automatically Unit tests and Feature tests using MySQL services.

Setup your first workflow with GitHub Action

Let's start to build your workflow file from scratch.

In your project directory, you need to create a new file in .github/workflows directory.

touch .github/workflows/laravel_phpunit.yml
Enter fullscreen mode Exit fullscreen mode

The yaml file will contain 3 main sections: name, on and jobs.

  • name: the name of the workflow;
  • on: the event that triggers the execution of the workflow. It could be also an array (so the action could be triggered by multiple events);
  • jobs: the list of jobs to be executed. By default, jobs are executed in parallel. You can define a kind of dependency trees with the needs directive, if you want to have some execution sequence instead of parallel execution. In this case we will use just one job. So, no dependency issue for us.

Name: define name

You can define the name of your workflow.
This name, is used in Github Actions user interface, for grouping reports and for managing workflows.

On: define events

You can define when to launch the workflow.
For example you can define , when you push your code on master and develop branch:

on:
  push:
    branches: [ master, develop ]
Enter fullscreen mode Exit fullscreen mode

You could define branches master, develop and all feature branches:

on:
  push:
    branches: [ master, develop. feature/** ]
Enter fullscreen mode Exit fullscreen mode

Or you could define also the pull request on master branch:

on:
  push:
    branches: [ master, develop, feature/** ]
  pull_request:
    branches: [ master ]
Enter fullscreen mode Exit fullscreen mode

Jobs: define Jobs

With a workflow you can define one or more jobs. A job is a specific task that needs to be executed in the workflow. You can configure to run multiple jobs in parallel or with some dependencies (for example: run the job “Test” only when the job “build assets” is completed)

Jobs: runs on

A job can be executed on a specific container that runs a operating system. For Laravel usually I use the latest version of Ubuntu.

jobs:
  laravel-test-withdb:
    runs-on: ubuntu-latest
Enter fullscreen mode Exit fullscreen mode

Jobs: mysql database

In the Job (jobs) section you can add also some service containers. For example, if you are creating a job that needs MySql service you could add service sub section.
This service could be used by your scripts and apps that runs in the current job.
For example in my case, I'm creating a job for running tests on my Laravel application. To do that, I need also a database. Sometimes I could add sqlite database, that it is easier to configure, but probably if you want to run test with the same database that you have on production, probably you could prefer to have a MySql instance.

    services:
      # mysql-service Label used to access the service container
      mysql-service:
        # Docker Hub image (also with version)
        image: mysql:5.7
        env:
          ## Accessing to Github secrets, where you can store your configuration
          MYSQL_ROOT_PASSWORD: ${{ secrets.DB_PASSWORD }}
          MYSQL_DATABASE: db_test
        ## map the "external" 33306 port with the "internal" 3306
        ports:
          - 33306:3306
        # Set health checks to wait until mysql database has started (it takes some seconds to start)
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
Enter fullscreen mode Exit fullscreen mode

The most important things to highlight are:

  • mysql-service is the label used to access the service container
  • mysql:5.7 we are using mysql image (version 5.7)
  • secrets.DB_PASSWORD accessing to Github secrets, where you can store your configuration
  • 33306:3306 we are mapping the "external" 33306 port with the "internal" one 3306. In the next steps we will need to use 33306 to access to the service, because the service container exposes the 33306.
  • mysqladmin ping : creating service container could take a while. So you need to be sure to wait until the mysql service is started and it is ready to accept connections. In this case we will retry for 3 times every 10 seconds and with a timeout of 5 seconds.

On and Service sections

Steps: define the steps

Each jobs has multiple steps. In each step you can “run” your commands or you can “use” some standard actions.

Following the yaml syntax, each step starts with “minus sign”. Step that uses standard action is identified with “uses” directive, steps that use custom commands are identified by “name” and “run” directive. “name” is used in the execution log as a label in the GitHub Actions UI, “run” is used for launch the command. With “run” directive you could define command with arguments and parameters. With “run” you can also list a set of commands.

    steps:
    - uses: actions/checkout@v2
    - name: Laravel Setup
      run: |
        cp .env.ci .env
        composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
        php artisan key:generate
        chmod -R 777 storage bootstrap/cache
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: mysql
        DB_DATABASE: db_test
        DB_PORT: 33306
        DB_USER: root
        DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
      run: |
        php artisan migrate
        vendor/phpunit/phpunit/phpunit
Enter fullscreen mode Exit fullscreen mode

Let me walk-through all steps in the steps section.

First step: git checkout

The first step is retrieve the sources. To do that, GitHub Actions has a standard action identified with “actions/checkout@v2”. To perform the action you need to set a step:

    - uses: actions/checkout@v2
Enter fullscreen mode Exit fullscreen mode

You can use also actions/checkout@master if you like to live on the edge.

Second Step: Laravel Setup

In Github workflow you need to think about a new fresh install of your web application every time you execute the workflow.
It means that you need to execute all things needed by a fresh installation like:

  • creating .env file;
  • install PHP packages;
  • generate the key (key:generate);
  • permissions fine tuning.

In Laravel you can use .env to store your keys and parameters for the environment configuration. For example: database connection parameters, cache connection parameters, SMTP configuration etc.
You can prepare your .env.ci with specific configuration for executing workflows and commit it on the repository.
Anyway we will override later some parameter like the connection with the database (some secret paramters like password or access tokens that you don't want to store in .env.ci).

For installing package I suggest you to use these options:

  • -q: avoid to output messages;
  • --no-ansi: disable ANSI ouput;
  • --no-interaction: avoid interactrions, useful for CI building;
  • --no-scripts: avoid execution of scripts defined in composer.json;
  • --no-suggest: don't show package suggestion;
  • --no-progress: don't show the progress display that can mess with some terminals or scripts which don't handle backspace characters;
  • --prefer-dist: prefer to download dist and try to skip git history.

Third step: execute migration and tests

Now you have your Laravel application installed in the runner and ready to use the MySQL service.
What we are going to do is:

  • running migration (create tables on the database);
  • execute tests using phpunit.

Both commands, probably, they will need to access to database.
To do that we need to be sure that all parameters are correctly configured.
In the last step you can add env sub section where you can list all your parameters. These parameters will override the parameters that you defined in your .env_file. And probably you want to avoid to hardcode some parameters in your workflow _yaml file. For that you could use the Settings -> Secrets functionality in your GitHub repository.
GitHub repository: Settings -> Secrets
In Settings/Secrets section you can define and list all your "secrets" and you can use those secrets you your yaml file with secrets.DB_PASSWORD. Please note that you need to use the secrets. prefix to access to secrets variables. DB_PASSWORD is the name of your secret.

    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: mysql
        DB_DATABASE: db_test
        DB_PORT: 33306
        DB_USER: root
        DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
      run: |
        php artisan migrate
        vendor/phpunit/phpunit/phpunit
Enter fullscreen mode Exit fullscreen mode

In this case:

  • DB_CONNECTION: mysql because you want to access to mysql database;
  • DB_DATABASE: db_test the name of the database, it needs to be the same defined in MYSQL_DATABASE in MySQL service section;
  • DB_PORT: 33306 the external port of the MySQL service. Do you remember it? In MySQL service section we defined 33306:3306 as "ports".
  • DB_USER: root is the main user configured in MySQL service;
  • DB_PASSWORD: we are using the secret named DB_PASSWORD.

With this configuration you can execute the migration and the phpunit. As you can see I'm using phpunit in the vendor directory.

Please write me your feedback or suggestion in the comment in order to improve this article.

Let’s automate everything!

Oldest comments (13)

Collapse
 
zaratedev profile image
Jonathan Zarate

Hi, Roberto.
I have the same configuration, but github actions throws me the exception.

 SQLSTATE[HY000] [2002] Connection refused (SQL: SHOW FULL TABLES WHERE table_type = 'BASE TABLE')
Enter fullscreen mode Exit fullscreen mode
Collapse
 
robertobutti profile image
Roberto B.

Hi Jonathan. Thank you for the feedback.
I woul like to check one thing. Assuming that all things are correct, how is confogured your DB_HOST?
On your .env file :
DB_HOST=127.0.0.1
Later i will be at my laptop and i will try to replicate your issue.

Collapse
 
zaratedev profile image
Jonathan Zarate

Yeah, the DB_HOST is 127.0.0.1

Can I share my configuration of my yaml file?

Thread Thread
 
robertobutti profile image
Roberto B.

Sure!

Thread Thread
 
zaratedev profile image
Jonathan Zarate
Thread Thread
 
robertobutti profile image
Roberto B.

I see you are using MYSQL_ALLOW_EMPTY_PASSWORD.
Just to give a try , instead of MYSQL_ALLOW_EMPTY_PASSWORD , please try to use a password.
MYSQL_ROOT_PASSWORD: secret

and setting the same passoword in the env section with DB_PASSWORD: secret

Thread Thread
 
zaratedev profile image
Jonathan Zarate

Hello Roberto.

I'm trying to run the laravel dusk tests, but sadly I keep getting the connection refused error.
The phpunit tests with database migrations trait run successful.

Thread Thread
 
robertobutti profile image
Roberto B.

Ok, I was able to replicate your problem with your YML file.
The difference is that the phpunit is executed via command line and dusk is executed with the webserver (php artisan serve). The problem is to pass the env to the right process.
With phpunit is enough to set the env before executing the command.
With dusk , you need to set the env variables before to launch the webserver.
Try to take a look this one:
github.com/roberto-butti/laravel7-...

Collapse
 
robertobutti profile image
Roberto B.

I created an Open Source tool for creating GitHub Actions workflow for Laravel application. The source code is: github.com/Hi-Folks/gh-actions-yam... . You can use a demo here: ghygen.hi-folks.dev/

Collapse
 
bobbyiliev profile image
Bobby Iliev

This tool is supper cool!

Collapse
 
robertobutti profile image
Roberto B.

Thank you! If you have any feedback or suggestion or feature request, feel free to raise your hand 🚀

Collapse
 
oandreyev profile image
Oleg Andreyev

Few things:

  • when running unit test you don’t need database, otherwise it’s not a unit tests. I’d suggest split test cases in test suites, e.g.: unit and functional (running unnecessary steps is just taking seconds and billed)
  • no needs to store test database as a secret, how cares about it? I personally use secrets for “real secret” as PAT
Collapse
 
robertobutti profile image
Roberto B.

thank you for the feedback. very useful and everything make sense. i will update the article with latest version of the tools, and i will also take in consideration your feedback. thank you again!!!