Using GitHub actions to run the tests of Laravel projects and packages
Tags: php, laravel, testing, github
Published: 2020-01-06 14:31:18

For many years we've relied on Travis to run the tests of our packages. For projects we used Circle CI. Recently we moved this responsibility from Travis and Circle CI to GitHub actions. In this blogpost I'd like to explain why and how we did this.

<!--more-->

## Why we moved to GitHub actions

There are three reasons why we made the move: costs, uniformity, and speed.

As mentioned in the intro, we used two services to run our test suites: Travis for open source and Circle CI for private client projects. When we started out creating open-source, Travis was (and still is I think) the de facto standard. It has a sensible configuration format with support for matrixes, and it's free for open source. For client projects, we picked Circle CI because it, like Travis, has an excellent way of configuring projects, but it's way cheaper than Travis for the tier that we needed.

We have already been using GitHub for source control, for both open-source and our private projects, for several years. For open-source projects, GitHub actions are free. With our Team subscription, we also [get 10 000 action minutes per month](https://github.com/features/actions#pricing-details).  Because we only need a fraction of that action time, using GitHub actions for our projects is basically free.

![](https://freek.dev/uploads/media/actions/usage.png)

By using the same system for our open source and private projects, we don't have to master two kinds of configuration anymore (Travis & Circle CI).

I don't have any benchmarks on this, but GitHub actions seem to be way faster then Travis to set up an environment and to complete a run of the test suite. Getting feedback more quickly is certainly much nicer.

What's also pretty cool is the fact that there are already [many useful GitHub actions](https://github.com/sdras/awesome-actions) that are open-sourced, making it easy to build workflows.

## Our GitHub workflow to run the tests

[Ignition](https://flareapp.io/ignition) is the default error page for Laravel applications. We want to be absolutely sure that it works for everyone. That's why we wrote an extensive test suite of both PHP and JavaScript tests that we run on each combination of PHP and Laravel that we support. Since a couple of days ago, those tests are being run on via GitHub actions. This enabled us to also run the test suite on multiple OS'es: Ubuntu and Windows.

To get started, a `.github/workflows` directory in must be created in a repo. Inside that directory, your GitHub workflow definitions can be put. 

Here's the content of [the configuration file we currently use for Ignition](https://github.com/facade/ignition/blob/412a979c2346a833b9ed0598e82214006dba3899/.github/workflows/run-tests.yml).

```yml
name: Run tests

on:
  push:
  schedule:
      - cron: '0 0 * * *'

jobs:
    php-tests:
        runs-on: ${{ matrix.os }}

        strategy:
            matrix:
                php: [7.4, 7.3, 7.2]
                laravel: [6.*, 5.8.*, 5.7.*, 5.6.*, 5.5.*]
                dependency-version: [prefer-lowest, prefer-stable]
                os: [ubuntu-latest, windows-latest]
                include:
                    - laravel: 6.*
                      testbench: 4.*
                    - laravel: 5.8.*
                      testbench: 3.8.*
                    - laravel: 5.7.*
                      testbench: 3.7.*
                    - laravel: 5.6.*
                      testbench: 3.6.*
                    - laravel: 5.5.*
                      testbench: 3.5.*
                exclude:
                    - laravel: 5.7.*
                      php: 7.4
                    - laravel: 5.6.*
                      php: 7.4
                    - laravel: 5.5.*
                      php: 7.4

        name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}

        steps:
            - name: Checkout code
              uses: actions/checkout@v1

            - name: Setup PHP
              uses: shivammathur/setup-php@v1
              with:
                  php-version: ${{ matrix.php }}
                  extension-csv: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
                  coverage: none

            - name: Install dependencies
              run: |
                  composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
                  composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest

            - name: Execute tests
              run: vendor/bin/phpunit

            - name: Send Slack notification
              uses: 8398a7/action-slack@v2
              if: failure()
              with:
                  status: ${{ job.status }}
                  author_name: ${{ github.actor }}
              env:
                SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    js-tests:
        runs-on: ubuntu-latest

        name: JavaScript tests

        steps:
            -   name: Checkout code
                uses: actions/checkout@v1

            -   name: Install dependencies
                run: yarn install --non-interactive

            -   name: Execute tests
                run: yarn run jest
                
            - name: Send Slack notification
              uses: 8398a7/action-slack@v2
              if: failure()
              with:
                  status: ${{ job.status }}
                  author_name: ${{ github.actor }}
              env:
                SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
 
### When to run the tests 
 
Let's break it down. On the top level, we start by specifying a name for this job.

```yml
name: Run tests
```

This name will be displayed in [the list of executed actions](https://github.com/facade/ignition/actions).

![](https://freek.dev/uploads/media/actions/name.png)


Next up, we'll specify when this job should be run.

```yml
on:
  push:
  schedule:
      - cron: '0 0 * * *'
```

Our test suite is run on two events. First, when code is pushed: this includes commits on the master branch as well as submitted pull requests. Secondly, we are going to run the test suite every day at midnight. We do this because, even when our code did not change, the code of our dependencies (so Laravel itself) might have changed, and we want to be sure Ignition works for newer versions of the dependencies as well.

There are some more types of events when an actions can be performed. Head to the [relevant section](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows) in GitHub action docs to learn more.

### Determining the environment

After the `on` part, we start defining the actual jobs that need to run.

```yml
jobs:
    php-tests:
    
    ...
    
    js-tests:
    
    ...
```

Let's start breaking down the `php-tests` job. First, we're going to specify on which environment the test should run.

```yml
runs-on: ${{ matrix.os }}

strategy:
    matrix:
        php: [7.4, 7.3, 7.2]
        laravel: [6.*, 5.8.*, 5.7.*, 5.6.*, 5.5.*]
        dependency-version: [prefer-lowest, prefer-stable]
        os: [ubuntu-latest, windows-latest]
        include:
            - laravel: 6.*
              testbench: 4.*
            - laravel: 5.8.*
              testbench: 3.8.*
            - laravel: 5.7.*
              testbench: 3.7.*
            - laravel: 5.6.*
              testbench: 3.6.*
            - laravel: 5.5.*
              testbench: 3.5.*
        exclude:
            - laravel: 5.7.*
              php: 7.4
            - laravel: 5.6.*
              php: 7.4
            - laravel: 5.5.*
              php: 7.4

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
```

Let's discuss that `matrix` first. Every version of PHP, Laravel, and OS we want to run our test on is specified. When a matrix is present, GitHub will create a job per combination of every item in the matrix. All those jobs will run concurrently.

![](https://freek.dev/uploads/media/actions/matrix.png)

It's important to realize that those entries in the matrix (`php`, `laravel`, `dependency-version` and `os`) are merely the names of variables that GitHub populates with one of the values given. Those variables can be used elsewhere in the job definition. You see a first example of this in the `runs-on` entry. 

```yml
runs-on: ${{ matrix.os }}
```

Here `matrix.os` will contain either `ubuntu-latest` or `windows-latest`.

In the `include` section, we define an additional variable called `testbench` of which the value depends on the value of the `laravel` variable. When `laravel` contains `6.*`, we'll set the value of `testbench` to `4.*`. I'll explain why we do this, a little biter later, when we are going to use that value.

At the moment of writing, some Laravel versions do not run well on PHP 7.4. In the `exclude` section, we specify that combinations that we don't want to run our tests for.

[By default](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstrategyfail-fast) a matrix will have the `fail-fast` set to `true`. This means that whenever a job fails, all the other ones will be canceled.

### Running the PHP tests

![](https://freek.dev/uploads/media/actions/steps.png)

With our matrix set up, we can now start preparing to run the actual tests. First, we are going to check out to code to our virtual test machine. 

```yml
- name: Checkout code
  uses: actions/checkout@v1
```

You can think of this as performing a `git clone` on the machine that will run the tests.

Next, we are going to set up PHP.

```yml
- name: Setup PHP
  uses: shivammathur/setup-php@v1
  with:
      php-version: ${{ matrix.php }}
      extension-csv: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
      coverage: none
```

For the step above, we use a 3rd party action `shivammathur/setup-php@v1`. This action is already becoming quite popular. I see it being used in a lot of PHP focused GitHub actions. It can be passed the PHP version you want to use. In our case, we are going to use the PHP version that is currently in the `matrix.php` variable.

After that, we are going to install our dependencies via composer.

```yml
- name: Install dependencies
  run: |
      composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
      composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
```

Using composer require and the `matrix.laravel` and `matrix.testbench` variables we are going to specify which version of Laravel and orchestra/testbench we want to use. [Orchestra/testbench](https://github.com/orchestral/testbench) is a cool package to allows testing package code as if it was installed in a full Laravel application.

Using `composer update,` we make sure that, depending on `matrix.depency`, the current or lower dependencies are being used.

Next up comes what we've all been waiting for: running the actual tests.

```yml
- name: Execute tests
  run: vendor/bin/phpunit
```

After running the test, and only if they fail, we'll send a Slack notification via a webhook.

```yml
- name: Send Slack notification
  uses: 8398a7/action-slack@v2
  if: failure()
  with:
      status: ${{ job.status }}
      author_name: ${{ github.actor }}
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

That `failure()` is one of [the available job status check functions](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#job-status-check-functions), and it makes sure that this step is only performed when the job fails.

The URL itself is defining as a secret. The value of a secret can be set at the `Secrets` section of the repo settings.

![](https://freek.dev/uploads/media/actions/secrets.png)


This is how the Slack notification looks like:

![](https://freek.dev/uploads/media/actions/slack.png)


### Running the JavaScript tests

Next to running PHP tests, we also run JavaScript tests.

```yml
js-tests:
    runs-on: ubuntu-latest

    name: JavaScript tests

    steps:
        -   name: Checkout code
            uses: actions/checkout@v1

        -   name: Install dependencies
            run: yarn install --non-interactive

        -   name: Execute tests
            run: yarn run jest
            
        - name: Send Slack notification
          uses: 8398a7/action-slack@v2
          if: failure()
          with:
              status: ${{ job.status }}
              author_name: ${{ github.actor }}
          env:
            SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

Because the `js-tests` is a separate job. That means it will be executed concurrently with the PHP tests. It will check out the code, install the dependencies via yarn and use `jest` to run the actual tests. Similar to the PHP tests, we send a notification when they should fail.

## Adding a badge

You'll be happy to know that, via the awesome shields.io service, you can add a badge to your readme indicating if the test workflow ran succesfully. Here's how that looks like on our readme.

![](https://freek.dev/uploads/media/actions/badge.png)

This is the markdown used for the badge in the screenshot.

```md
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/facade/ignition/run-php-tests?label=Tests)
```

## In closing

My colleague [Ruben](https://twitter.com/rubenvanassche) did a lot of work researching GitHub actions. If you want to know how we use GitHub actions to test a typical private Laravel application, [head over to Ruben's blog](https://rubenvanassche.com/getting-started-with-github-actions/).

I think GitHub actions are pretty neat, and I'm sure we'll use them for other use cases (fixing code style issues, performing code quality checks, deploying, ...) in the future as well.

For more information on how to work with actions, head to [the official documentation on GitHub](https://help.github.com/en/actions).