How to speed up your Laravel test suite by using the GitHub Actions cache
Jon Milsom • Saturday 14th October 2023
php laravel github-actionsWhen you're working in software development, especially if you are part of a team, it's good practice to implement
Continuous Integration "CI"; continually running your test suite after each git push
or each time a PR is opened or
updated, but as your test suite grows it can start to slow down the test stage of your pipeline, meaning you have to
wait before merging to main and deploying confidently.
Speed is key and whilst you'll eventually need to parallelise the execution of your test suites across multiple nodes, you should optimise each step in the linear process as well.
Normal steps in a Laravel test suite run via GitHub actions
Step 1 - Setup the environment and pull in the code base
Step 2 - Install dependencies via composer
Step 3 - Execute the tests using phpunit or pest
Optimising the composer install stage (step 2 from the list above)
Once your project is setup and any composer packages installed, you'll only need to change your composer config again if you want to add a new package to support new functionality or update to a newer version of an existing package if there is, for example, a security patch available.
By default your composer install
command will run each time your test suite does, but it's likely to be doing the same
work over and over - re-installing the same set of packages and versions as defined in your composer.json
and
composer.lock
files.
We can remove this slow, repetitive work by caching the output of the composer install
command, and using the cached
version for future test runs.
How to cache your composer install output using GitHub Actions
Conveniently, there is a built-in GitHub Action to help save to and restore from the GitHub cache.
Each repository can have up to 10GB of cache data and will automatically tidy up after itself by (a) deleting cached data that is not accessed after more than a week and (b) evict older cached items when the cache becomes full:
"A repository can have up to 10GB of caches. Once the 10GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted."
What additional steps do I need to add to my GitHub action?
Very simply, you need to check the cache for an existing entry before you run composer install
. If an entry exists
you can restore the files from the cache and skip over the slow composer install
command.
You also need to save to the cache after running composer install
if there was no pre-existing entry in the cache -
this will make sure the next time your GitHub actions runs, it won't repeat the process.
You don't need to delete from the cache at any point - GitHub will do this for you.
What cache key should I use?
The cache key is used when saving to and restoring from the cache, it acts as a pointer to the correct chunk of data.
It's important that the cache key changes when the content of the cache changes - just like pre-compiled CSS and JS
files in frontend development we can use a hash
to represent the content of the files neatly, and provide a fixed
number of characters in our cache key.
An example of a good cache key might be a hash of both the composer.lock
and composer.json
files, with a prefix that
makes sense to other members of your team.
key: composer-v1-${{ hashFiles('composer.json', 'composer.lock') }}
It's also good to represent the context that the cache was built in; for example the operating system. This will make sure any slight differences in the environment (for example, availability of PHP extensions) are included in the cache key:
key: composer-v1-${{ runner.os }}-${{ hashFiles('composer.json', 'composer.lock') }}
Finally, if you are using a matrix strategy to test against multiple versions of PHP, you should also include the PHP version in the cache key.
key: composer-v1-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('composer.json', 'composer.lock') }}
Putting it all together
Here's an example of a GitHub actions .yaml file before and after implementing caching:
Before
- name: Composer install
run: composer install
After
- name: Try restore the vendor directory from cache
uses: actions/cache/restore@v3
id: composer-github-cache
with:
path: vendor
key: composer-v1-${{ runner.os }}-${{ hashFiles('composer.json', 'composer.lock') }}
- name: Composer install
if: steps.composer-github-cache.outputs.cache-hit != 'true'
run: composer install
- name: Save the vendor directory to the cache
if: steps.composer-github-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v3
with:
path: vendor
key: composer-v1-${{ runner.os }}-${{ hashFiles('composer.json', 'composer.lock') }}
A few extra lines of yaml, a huge speed improvement! 🚀