orchestra/testbench
Orchestra Testbench is the de-facto helper for writing tests for Laravel packages, providing a lightweight Laravel application skeleton so you can verify your package's service providers, routes, and config.
Testbench is development infrastructure: it boots a Laravel application inside your package’s test suite so you can exercise providers, routes, commands, migrations, config merging, views, and assets without maintaining a separate Laravel app. Day-to-day leverage is high once the version matrix is aligned; friction is mostly in matching Testbench to Laravel/PHPUnit and understanding that this is not a runtime dependency.
Minimal setup:
composer require --dev orchestra/testbench:^10.0
Use ^9 for Laravel 11, ^10 for Laravel 12, ^11 for Laravel 13. Check the release notes because Testbench Core and PHPUnit minimums often move faster than the Laravel constraint.
<?php
namespace YourPackage\Tests;
use Orchestra\Testbench\TestCase as BaseTestCase;
use YourPackage\YourServiceProvider;
abstract class TestCase extends BaseTestCase
{
protected function getPackageProviders($app): array
{
return [YourServiceProvider::class];
}
protected function getEnvironmentSetUp($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
}
}
vendor/bin/phpunit
vendor/bin/pest
vendor/bin/testbench package:test
Where to look first:
tests/TestCase.php: provider registration and environment setup.testbench.yaml: package discovery, providers, migrations, seeders, Workbench config.workbench/: a local Laravel app for manual smoke tests and browser/HTTP checks.First useful use case: register the package provider, set a config value, hit a package route or run a package command, and assert the result. That replaces the old “create a throwaway Laravel app, install the package via path repository, and test manually” loop.
Provider-driven bootstrapping. Almost every package test starts with getPackageProviders(). Keep it explicit rather than relying on package discovery:
protected function getPackageProviders($app): array
{
return [
YourPackage\YourServiceProvider::class,
YourPackage\TestingServiceProvider::class,
];
}
Config and environment setup. Use getEnvironmentSetUp() for baseline test config. Use attributes for test-specific overrides:
use Orchestra\Testbench\Attributes\WithConfig;
#[WithConfig('services.example.endpoint', 'https://example.test')]
public function test_it_uses_configured_endpoint(): void
{
// ...
}
For config timing, remember that newer Testbench versions load #[WithConfig] after the application has booted by default so providers can merge config first. Use defer: false only when the config must exist before provider boot.
Database tests. Use Laravel’s normal testing traits plus Testbench’s migration helpers:
use Illuminate\Foundation\Testing\RefreshDatabase;
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
For package migrations, this is usually cleaner than copying migrations into a host app. If your package ships Laravel migrations, loadLaravelMigrations() is available. testbench.yaml can also declare migrations and seeders: true when the skeleton should seed automatically.
Workbench for manual and integration testing. The workbench/ directory is the day-to-day replacement for “install this package in a fresh Laravel app.” Use it for:
Typical loop:
vendor/bin/testbench workbench:serve
vendor/bin/testbench package:test
vendor/bin/testbench package:purge-skeleton
Use package:purge-skeleton when the skeleton or vendor symlink gets stale; it is the reset button.
Artisan command tests. Use Laravel’s command assertions rather than shelling out to php artisan:
public function test_install_command(): void
{
$this->artisan('your-package:install')
->expectsOutput('Package installed.')
->assertSuccessful();
}
Testbench gives the command a booted Laravel app with your provider registered. If the command depends on vendor package discovery, use #[UsesVendor] on the test class.
Pest integration. The same TestCase works with Pest:
uses(YourPackage\Tests\TestCase::class)->in(__DIR__);
Attributes as workflow shortcuts. Prefer attributes over deprecated PHPUnit annotations:
#[WithConfig(...)] for targeted config changes.#[UsesVendor] when the test needs vendor discovery/symlinked vendor behavior.#[WithFixtures] to auto-load a fixture file for the test.Isolation helpers. For tests that need process-level behavior, Testbench exposes Orchestra\Testbench\remote() for running isolated commands and terminate()/bail() for pnctl-enabled tests. These are extension points for package installers, long-running commands, and CLI integration tests, not everyday unit tests.
Version alignment is the first failure mode. Testbench majors track Laravel majors, but the release notes show tighter coupling to Testbench Core, PHPUnit, and minimum Laravel patch versions. A package that passes on Laravel 12.40 may fail on 12.28 because Testbench requires a newer framework. Pin Testbench deliberately and run the matrix in CI; do not let composer update silently choose a Testbench major.
Do not use php artisan inside package tests. There is no host app’s artisan binary in the package context. Use $this->artisan(...), vendor/bin/testbench, or Testbench’s console helpers. Shelling out to an external Laravel app bypasses the booted test application and hides provider/config bugs.
Skeleton and symlink state can go stale. Workbench relies on a generated skeleton and, in some flows, a vendor symlink. On Windows, symlink removal has historically been fragile. If tests behave as if old providers, config, or assets are present, run vendor/bin/testbench package:purge-skeleton and rebuild Workbench before debugging deeper.
Static state leaks between tests. Testbench flushes a lot: Eloquent model states, Str, validator state, JsonResource state, and more in recent releases. Your package can still leak its own static caches, singleton overrides, or config mutations. Add explicit teardown for package-level static state, especially when using --parallel.
#[WithConfig] timing changed. In newer versions it is applied after application boot by default to allow providers to merge configuration first. If you are testing a provider that reads config during register(), you may need defer: false; if you are testing normal config merging, leave the default.
#[UsesVendor] is for vendor-discovery tests. It is not a general “make it work” attribute. It exists because some package behavior depends on the app being booted with vendor discovery/symlinked vendor context. If a test fails with a BindingResolutionException around UsesVendor, check the Testbench/Core patch level — this has been fixed in several releases.
Parallel testing needs clean fixtures. WithFixtures and --parallel compatibility has been patched over time. If fixtures or SQLite journal files collide, check the Testbench patch version and avoid sharing mutable fixture files across parallel workers.
testbench.yaml is not a substitute for explicit tests. It configures the skeleton and package runner. It does not assert your provider registered a binding or your command output is correct. Use it to reduce boilerplate; keep behavioral assertions in PHPUnit/Pest.
Debugging checklist. When a package test fails, inspect in this order: Testbench/Laravel version compatibility, getPackageProviders() contents, getEnvironmentSetUp() config, testbench.yaml providers/migrations/seeders, stale Workbench skeleton, then static state leakage. Use --filter on package:test and a minimal TestCase before adding traits or attributes.
Extension points worth knowing. Custom skeletons, Orchestra\Testbench\Foundation\Env::flushState(), package_version_compare(), php_version_compare(), and remote() closures are useful for packages that test installers, version-dependent behavior, or process isolation. They are power tools; most packages should stay with a simple TestCase, provider registration, and RefreshDatabase.
How can I help you explore Laravel packages today?