skip to content

In Laravel parallel testing, what do the ParallelTesting hooks and ParallelTesting::token() give you, and when do you need them?

level: seniorimportance: nice to knowfreq 18%

answer

  1. per-process setup and teardown
  2. setUpProcess, setUpTestCase, setUpTestDatabase
  3. only fire in a parallel run
  4. token() returns false when sequential
  5. closure parameters matched by name

basics

~20 s

ParallelTesting hooks run closures on process or test-case setup and teardown, and when a per-process test database is created, only in parallel runs. token() returns the worker's identifier for separating files, indexes or Redis databases, and false outside a parallel run.

solid answer

~40 s

`ParallelTesting` hooks are registered in a service provider's `boot()` and fire **only in a parallel run**: `setUpProcess` and `tearDownProcess` per worker, `setUpTestCase` and `tearDownTestCase` around every test, `setUpTestDatabase` when a per-process test database has just been created, and `setUpTestDatabaseBeforeMigrating` just before it. Laravel calls the closures through the container, passing `$token`, `$testCase` and `$database` **by parameter name**. `ParallelTesting::token()` returns the worker's token, or `false` in a sequential run. You need them for resources Laravel does not isolate itself: it already separates the database, the cache prefix and the compiled Blade view path, but not a storage directory, a Redis database used directly, a search index or a port.

code

php · 27 lines
php
<?php

namespace App\Providers;

use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\ServiceProvider;
use PHPUnit\Framework\TestCase;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Each worker gets its own search index prefix and export folder.
        ParallelTesting::setUpTestCase(function (int $token, TestCase $testCase) {
            config([
                'scout.prefix' => "test_{$token}_",
                'filesystems.disks.local.root' => storage_path("app/private/test_{$token}"),
            ]);
        });

        // Remove the worker's export folder when its process finishes.
        ParallelTesting::tearDownProcess(function (int $token) {
            File::deleteDirectory(storage_path("app/private/test_{$token}"));
        });
    }
}

go deeper

for a junior

Know that the ParallelTesting facade exists for parallel runs and that each worker has a token identifying it.

for a middle

Explain which hooks exist, that they only fire in parallel runs, and that token() is false when tests run sequentially.

for a senior

Identify shared resources Laravel leaves alone, such as files, a search index or a direct Redis database, and segment them with the token in the right hook.

for a principal

Decide how much parallel-only setup the team tolerates, preferring resources that isolate themselves, so the suite behaves the same sequentially and in parallel.

## Why hooks exist When `php artisan test --parallel` splits a suite across worker processes, Laravel keeps workers apart by giving each one a **token**, a small number such as 1, 2 or 3. Out of the box, the framework's parallel-testing service provider uses that token to give every worker: - its own database, named `{database}_test_{token}`, when a test uses a database reset trait and the connection is not in-memory SQLite; - its own cache prefix, extended with `test_{token}_`; - its own compiled Blade view folder, `test_{token}` under the compiled-view path. Anything else your tests touch is shared by every worker. The `ParallelTesting` facade lets you add the same kind of isolation for your own resources. ## The hooks | Hook | Runs | Closure receives | |---|---|---| | `setUpProcess` | once per process token, before the workers run tests | `$token` | | `setUpTestCase` | before each test, after its application is created | `$token`, `$testCase` | | `setUpTestDatabaseBeforeMigrating` | when a per-process test database was just created | `$database`, `$token` | | `setUpTestDatabase` | right after that, in the same test-case setup | `$database`, `$token` | | `tearDownTestCase` | after each test | `$token`, `$testCase` | | `tearDownProcess` | once per process token, after the run | `$token` | Three details trip people up: 1. **Only in parallel.** Every hook is wrapped in a check that the `LARAVEL_PARALLEL_TESTING` flag is set and a token exists. In a plain `php artisan test` run none of them fires, so setup that every run needs does not belong here. 2. **Named parameters.** Laravel invokes each closure through the container with an array keyed `token`, `testCase` and `database`. The parameter **names** must match; their order does not matter, and other type-hinted parameters are resolved from the container. 3. **Database hooks fire on creation, before the test's own trait.** `setUpTestDatabase` runs only when Laravel had to create the per-process database; later runs that reuse `boxes_test_2` never fire it again, so it is the wrong place for per-test data. It also runs during test-case setup, before the test class's database reset trait. With `RefreshDatabase`, the first test in each process then runs `migrate:fresh`, which drops every table, so rows seeded in this hook do not survive; seed through the reset trait's own options instead. ## The token itself `ParallelTesting::token()` returns the current worker's token. Its declared return type is `string|false`: inside a parallel worker it is read from the `TEST_TOKEN` variable the runner sets, and outside a parallel run it is **`false`**. Code that builds a path like `"exports/test_{$token}"` must handle the sequential case, or every sequential run will write to `exports/test_`. ## Where you need them In a subscription-box app, typical shared resources are: - **Generated files**, such as packing-slip PDFs written to a real directory, which two workers would overwrite; - **A search index** for the box catalogue, where Scout's `prefix` config (`SCOUT_PREFIX`) can be set per worker; - **A Redis database** used directly for rate limits or counters, separate from the cache; - **A local port**, for example a stub shipping-label server that each worker must start on its own port. Registration lives in a service provider's `boot()`, usually `AppServiceProvider`, because the hooks must be registered on each freshly booted application. Keeping them there is harmless in production: outside a parallel test run they are never called. ## The built-in isolation uses the same hooks Laravel's own per-process isolation is written with exactly this API. Its parallel-testing service provider, registered only when the app runs in the console, registers a `setUpProcess` hook that drops the old test database when `--recreate-databases` was passed, and a `setUpTestCase` hook that creates the per-process database if needed and switches the connection to it. Further `setUpTestCase` hooks rewrite the cache prefix and the compiled-view path, and a `tearDownProcess` hook deletes the per-process view folder. Reading that provider is the quickest way to see what a well-behaved hook looks like. ## Checklist before adding a hook - Is the resource already isolated? Database, cache prefix and compiled views are. - Does the setup belong to every run? Then it goes in the test class or `Tests\TestCase`, not a parallel-only hook. - Does the code tolerate `token()` returning `false`? - Is the per-worker resource cleaned up in `tearDownProcess` if it would otherwise accumulate? Interviewers rarely ask for the full hook list; they ask how you would stop two workers colliding on something Laravel does not know about, and a precise answer names `ParallelTesting::token()` and the right hook.

  • Why do rows seeded in ParallelTesting::setUpTestDatabase vanish in tests that use RefreshDatabase?
    The hook fires during test-case setup, before the class's database trait runs. `RefreshDatabase` then runs `migrate:fresh` on the first test in each process, dropping every table, including the seeded rows. The hook also fires only when the database was just created, so reused databases never see it. Seed through the reset trait's own seeding options instead.
  • A hook closure declared as function ($id) fails with a BindingResolutionException; why?
    Laravel calls hook closures through the container with arguments keyed by name: `token`, `testCase` and `database`. A required, untyped parameter called `$id` matches none of them and the container cannot resolve it, so it throws. Rename the parameter to `$token`.

saying these in an interview costs you the question

  • ParallelTesting hooks also run in a normal sequential test run.
  • ParallelTesting::token() returns 0 or 1 when tests run sequentially.
  • setUpTestDatabase runs before every test, so it suits per-test data.
  • Laravel isolates files and Redis per process, so hooks are never needed.
  • Hook closure parameters are passed by position, so their names do not matter.