How to run your Laravel Dusk tests in parallel with GitHub Actions
Laravel Dusk tests are slow, running them in parallel can speed them up significantly.
This post explains how to run your Dusk tests in parallel using GitHub actions.
This post assumes you already have your Dusk tests running in your CI pipeline.
Multiple runners with a matrix
There is no --parallel
flag for Dusk tests.
So instead, we use multiple runners and divide the tests amongst them.
In your GitHub Actions workflow file, define a matrix of runners:
jobs:
tests:
runs-on: "ubuntu-20.04"
strategy:
fail-fast: false
matrix:
parallel-runner: [
"dusk-1/5",
"dusk-2/5",
"dusk-3/5",
"dusk-4/5",
"dusk-5/5",
]
I recommend using fail-fast: false
so you get a complete list of failed tests.
That way you can fix them all in one go.
We pass the name of the runner to the process that runs our Dusk tests:
- name: Run Dusk Tests
run: php artisan dusk
env:
PARALLEL_TEST_RUNNER: ${{ matrix.parallel-runner }}
Chunking tests with a PHPUnit extension
Now that we have multiple runners, and each runner is receiving its name, we can figure out which tests each runner should run.
We can do this using a PHPUnit extension and two PHPUnit hooks: BeforeFirstTestHook
and BeforeTestHook
:
<?php
namespace Tests\Support;
use Illuminate\Support\Str;
use PHPUnit\Runner\BeforeFirstTestHook;
use PHPUnit\Runner\BeforeTestHook;
class RunTestsInParallel implements BeforeFirstTestHook, BeforeTestHook
{
public static bool $shouldSkipCurrentTest = false;
private static int $i = 0;
private static int $runnerNumber = -1;
private static int $runnersCount = -1;
public function executeBeforeFirstTest(): void
{
$runnerName = getenv('PARALLEL_TEST_RUNNER'); // for example: "dusk-2/5"
if (! $runnerName) {
return;
}
[static::$runnerNumber, static::$runnersCount] = Str::of($runnerName)->afterLast('-')->explode('/');
throw_if(static::$runnerNumber === 0, 'Invalid runner name, must start at 1: '.$runnerName);
throw_if(static::$runnerNumber > static::$runnersCount, 'Invalid runner name: '.$runnerName);
echo 'Running tests in parallel, chunk '.static::$runnerNumber.'/'.static::$runnersCount.'...'.PHP_EOL.PHP_EOL;
}
public function executeBeforeTest(string $test): void
{
if (static::$runnerNumber === -1) {
return;
}
static::$i++;
static::$shouldSkipCurrentTest = static::$i % static::$runnersCount !== static::$runnerNumber - 1;
}
}
Register this PHPUnit extension in phpunit.dusk.xml
.
If you don't have this file yet, create it in the root of your project:
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Browser Test Suite">
<directory>./tests/Browser</directory>
</testsuite>
</testsuites>
<extensions>
<extension class="Tests\Support\RunTestsInParallel"/>
</extensions>
</phpunit>
And now our DuskTestCase
can skip tests:
protected function setUp(): void
{
if (RunTestsInParallel::$shouldSkipCurrentTest) {
$this->markTestSkipped();
}
parent::setUp();
}
And that's it, parallel Dusk tests.
The beauty of this approach is that you can run your tests like you'd normally do.
In your local environment, the PARALLEL_TEST_RUNNER
variable won't be set, and all tests will be run.
In CI, the variable is set, and tests will automatically be chunked based on how many runners you have defined.
Uploading screenshots of failed tests
When a Dusk test fails, it takes a screenshot. These screenshots are usually pretty handy for debugging. If you want to see the screenshots your CI workflow makes, you can upload them as an artifact:
- name: Upload Dusk fail screenshots
if: failure()
uses: actions/upload-artifact@v2
with:
name: dusk-fail-screenshots
path: tests/Browser/screenshots/failure-*
retention-days: 1
Notice how you don't have to give the artifact a unique name for each parallel runner. By default, GitHub will append to the artifact instead of overwriting it. You'll get a single zip file full of screenshots even if you have multiple workflow runners.
Also running PHPUnit tests in parallel
You can also use the approach described in this post to run your unit tests in parallel.
Laravel offers the --parallel
flag for normal PHPUnit tests, but that won't help much because GitHub runners aren't very powerful.
If you also run your unit tests in parallel, your workflow will look something like this:
jobs:
tests:
runs-on: "ubuntu-20.04"
strategy:
fail-fast: false
matrix:
parallel-runner: [
"phpunit-1/2",
"phpunit-1/2",
"dusk-1/5",
"dusk-2/5",
"dusk-3/5",
"dusk-4/5",
"dusk-5/5",
]
You'll have to make sure your PHPUnit runners don't run your Dusk tests and vice versa. You can do that like this:
- name: Run PHPUnit tests
if: startsWith(matrix.parallel-runner, 'phpunit')
run: vendor/bin/phpunit
env:
PARALLEL_TEST_RUNNER: ${{ matrix.parallel-runner }}
- name: Run Dusk Tests
if: startsWith(matrix.parallel-runner, 'dusk')
run: php artisan dusk
env:
PARALLEL_TEST_RUNNER: ${{ matrix.parallel-runner }}
If you've done that, you just have to register the RunTestsInParallel
extension in your phpunit.xml
and skip the tests in your TestCase.php
, and you'll be golden.