Testing Laravel packages with GitHub Actions

Let's take a look at testing Laravel Packages with GitHub Actions

GitHub Actions are hot! Since my last post, we started using it everywhere we can! Today we're going to look at how you can use GitHub Actions to test Laravel Packages. You might think this would be the same as using Actions on a default Laravel project, but some changes should be made to make it work.

Disclaimer: If you haven't read my previous post, please, read it first! It explains the basic principles of Actions which we continue to use in this post.

Welcome to the matrix 🔴-🔵

Let's get started and continue where we left the previous time: creating a testing workflow. First, we create a new workflow file in our repository:  .github/workflows/testing.yml and start with a basic template:

name: Test

on: [push]

jobs:
    test:
        runs-on: ubuntu-latest

Time for the first difference between application and package testing, let's add a strategy key to the test section:

test:
    runs-on: ubuntu-latest
    strategy:
        fail-fast: true
        matrix:
            php: [7.2, 7.3, 7.4]
            laravel: [5.8.*, 6.*]
            dependency-version: [prefer-lowest, prefer-stable]

Wow! There's quite a lot happening here. Let's go through it step-by-step. What we're trying to achieve is matrix testing, simply said: we try to run our tests on a lot of different configurations so we can be sure that our package is working in different environments.

Each configuration will spawn a GitHub Action job with specific configuration options. GitHub Actions will inject the configuration options for each configuration as variables so we can use them through the workflow. In this case, the configuration options are: the PHP version, the Laravel version, and if we desire our composer dependencies to be stable or the lowest possible.

An example configuration could look like this:

  • PHP 7.4
  • Laravel 5.8.*
  • prefer-stable dependencies

In our workflow under the strategy section we create a matrix section, here we can specify the configuration options by giving them a key and an array of options from which GitHub Actions will create configurations.

When this workflow runs, it will spawn a total of 12 jobs! You can calculate this as such:

3 PHP versions (7.2, 7.3, 7.4) * 2 Laravel versions (5.8.*, 6.*) and 2 composer flags (prefer-lowest, prefer-stable) = 12.

Next to the matrix key in the strategy section we've added that fail-fast will be true. This setting is not required but becomes quite handy when you're spawning a lot of jobs. If one of your configurations fails, then all the other configurations of that workflow will immediately fail. This will reduce the number of jobs running without purpose because they will also probably fail.

When testing a Laravel package, we also going to need Testbench. The problem is Laravel 5.8.* requires Testbench 3.8.* and Laravel 6.* requires Testbench 4.*. We can add an include section to our matrix in which we simply say: "if this variable is this value, then add another variable with this value." In our workflow, it looks like this:

matrix:
    php: [7.2, 7.3, 7.4]
    laravel: [5.8.*, 6.*]
    dependency-version: [prefer-lowest, prefer-stable]
    include:
        -   laravel: 6.*
            testbench: 4.*
        -   laravel: 5.8.*
            testbench: 3.8.*

Using matrix variables

So we defined our configurations, now in every job, we will have access to the following variables: php, laravel, dependency-version, and testbench. Let's try this out! We're going to give our job a name with the variables of our configuration. This can be done by adding a new key name in the test section:

test:
    runs-on: ubuntu-latest
    name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}
    strategy:
        ...

As you can see the configuration variables are scoped by the matrix keyword, with our example configuration from above the name of the job will look like: PHP 7.4 - Laravel 5.8.*

Stepping through the matrix

Everything is set up let's start testing! First we will be defining the steps that will run in each job. We start with checking out the code from GitHub:

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

Then we'll install the PHP version of our configuration:

-   name: Setup PHP
    uses: shivammathur/setup-php@v1
    with:
        php-version: ${{ matrix.php }}

It is time for the next difference between application and package testing with Actions! In the application workflow, we used the build-in PHP version of Actions, which at the time of writing is 7.3. If we want to matrix test our PHP version, then we have to set it up. This can be done by using the setup-php action. It takes the php-version as an option which we fill in with our configuration variable ${{ matrix.php }}.

Want to add some extra extensions to PHP or change some ini values? Then check the GitHub page of the setup-php action for more information.

Next, we install the composer dependencies:

-   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

We first require a specific Laravel and Testbench version before installing our other dependencies with the prefer-stable or prefer-lowest composer flag.

Of course, we can also cache our dependencies, so the next time the Action runs it will even run faster. In the application workflow, we had access to a composer.lock file that we used as a key to identify if the cache entry existed. But we don't commit a composer.lock file to the repositories of packages.

The solution is to use the hash of the composer.json file and the configuration variables. The step will in the end look like this:

-   name: Cache dependencies
    uses: actions/cache@v1
    with:
        path: ~/.composer/cache/files
        key: dependencies-${{ matrix.dependency-version }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}

Do not forget to put this step before the Install dependencies step. Otherwise your cache would not be used.

What if one of the packages in composer.json get's bumped with a new version? We would expect that our cache will be invalidated, but that's not going to happen 😞. The only times our cache will be invalidated will be when our composer.json file changes. Now, this is not that big of a problem. Every week GitHub purges the caches and Composer will always use the latest dependencies possible even if they are not cached.

Now for the last step of the workflow, we run our tests:

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

If everything is going well, your workflow should look like this:

Badges

Who doesn't like badges? You can create them for your GitHub Actions. Head over to shields.io and search for: GitHub Workflow Status.

Conclusion

This powerful yet straightforward workflow works for 90% of our packages. Matrix testing provides us the trust that our packages run on the environments we defined. You can find the workflow we've build here.

If you want to read more about testing packages, then head over to Freek's blog, where he adds Github Actions to Laravel Ignition. In the next part, we will take a look at creating our own action for the checking and fixing of our PHP styling. See you then!