NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Testing for Laravel

Publish sandbox

For a test that publishes, installs or generates files. Every pest --parallel process boots from the same Testbench skeleton: Testbench requires each config/*.php there and migrate runs each file in database/migrations. A test that writes into it fails some other test when a file appears or vanishes mid-boot (“Path must not be empty” in LoadConfiguration, a migration run twice), and a leftover is loaded by every later run. The UsesPublishSandbox concern points the paths at a directory of its own per test and deletes it when the test ends:

<?php

use RoundlyConsulting\Testing\Concerns\UsesPublishSandbox;

// tests/Pest.php — Pest binds a test case per directory
uses(TestCase::class, UsesPublishSandbox::class)->in('Publish');

// tests/Publish/PublishTest.php
it('publishes into the sandbox, never the shared skeleton', function (): void {
    $this->assertPublishesIntoSandbox('credits-config', 'credits-migrations');
});

it('publishes the config', function (): void {
    $this->artisan('vendor:publish', ['--tag' => 'credits-config'])->assertSuccessful();

    expect($this->publishSandbox('config/credits.php'))->toBeFile();
});

Or use UsesPublishSandbox on a directory-scoped base case. It is package-only — it assumes a Testbench app.

API

MemberBehaviour
publishSandbox(string $path = '')The sandbox root (under sys_get_temp_dir()), or a path inside it. Throws LogicException before the application exists.
assertPublishesIntoSandbox(string ...$tags)The pin: every sandboxed helper (config_path(), database_path(), app_path()) resolves inside the sandbox, and so does every destination each tag maps (ServiceProvider::pathsToPublish(null, $tag)). A tag that maps nothing fails — an empty mapping would pass without checking anything.
publishSandboxPaths(): arrayOverride to narrow what moves: any of config, database, app (default: all three). Anything else throws InvalidArgumentException.
preparePublishSandbox(string $root): voidOverride to put files the host already has into the sandbox, before the providers register.
use RoundlyConsulting\Testing\Concerns\UsesPublishSandbox;

// tests/Publish/PublishTestCase.php — config/ only, seeded with a file the host already has
abstract class PublishTestCase extends TestCase
{
    use UsesPublishSandbox;

    protected function publishSandboxPaths(): array
    {
        return ['config'];
    }

    protected function preparePublishSandbox(string $root): void
    {
        file_put_contents($root.'/config/credits.php', "<?php\n\nreturn ['host' => true];\n");
    }
}

How it works

  • The paths move before the providers register, in getPackageProviders(). A provider that maps config_path(...) or database_path(...) while it registers fixes its destination then; a sandbox set in defineEnvironment() would move the helper but leave that publish pointed at the skeleton. That is why the pin checks the publish mapping, not only the helper. The configuration has already loaded from the skeleton by then, so the suite boots as usual.
  • app/ moves after the application namespace is resolved. Laravel derives it by matching app/ against the skeleton’s composer.json, which a sandbox would not match, so make:* generators keep producing App\… classes.
  • Every test gets its own sandbox, and it is deleted when the test ends.

Overriding getPackageProviders()

An override of getPackageProviders() must still reach the trait’s. In a subclass, parent::getPackageProviders($app) does. In the class that uses the trait, the override replaces the trait’s method, so alias it and call the alias. A test case that hides it is refused in setUp with a LogicException, rather than letting the providers register against the skeleton. Declare the providers in packageProviders() and there is nothing to override:

use RoundlyConsulting\Testing\Concerns\UsesPublishSandbox;
use RoundlyConsulting\Testing\PackageTestCase;

// Overriding getPackageProviders() in the class that uses the trait: alias the trait's method
abstract class PublishTestCase extends PackageTestCase
{
    use UsesPublishSandbox {
        getPackageProviders as sandboxProviders;
    }

    protected function getPackageProviders($app): array
    {
        // … your own logic
        return $this->sandboxProviders($app);
    }
}

Not covered: storage/, lang/ and shared temp directories. Storage::fake() already appends the parallel token; find a file in a shared temp directory by a unique marker, never by a before/after file count.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.