Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Testbench Laravel Package

graham-campbell/testbench

Laravel TestBench by Graham Campbell provides testing functionality for Laravel packages, built on PHPUnit, Mockery, Orchestral Testbench, and Laravel Testbench Core. Supports PHP 7.4-8.5, PHPUnit 9-11, and Laravel 8-13.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install as a dev dependency only

composer require --dev graham-campbell/testbench:^6.3

There is nothing to register — no service provider, no config file, no artisan command. The package is a thin layer of abstract test cases on top of orchestral/testbench + PHPUnit + Mockery.

2. Check the version matrix before you install. The package is tightly pinned to Laravel/PHP/PHPUnit combinations:

TestBench Laravel PHPUnit PHP
6.3 8–13 9–11 (+12 per changelog) 7.4–8.5
5.7 5.7–9 6–9 —
4.0 5.5 6 —

If your package still supports Laravel 9, you are on 5.7. If you are on Laravel 10, you need 6.x.

3. Where to look first (in this order)

  • src/AbstractTestCase.php — the base every other case extends; adds Mockery lifecycle handling and a few assertions.
  • src/AbstractPackageTestCase.php — the one you will actually use. It builds the app from a list of service providers you declare.
  • src/AbstractAppTestCase.php — for testing against a full application skeleton rather than a package.
  • Graham Campbell's own packages (Laravel-Credential, Laravel-Collector, Laravel-Flysystem, etc.) — the README explicitly says "you may see an example of implementation in pretty much all of my Laravel packages", and that is the real documentation. Open any of their tests/TestCase.php files.

4. First use case: a base TestCase for your package

// tests/TestCase.php
namespace MyVendor\MyPackage\Tests;

use GrahamCampbell\TestBench\AbstractPackageTestCase;
use MyVendor\MyPackage\MyPackageServiceProvider;

abstract class TestCase extends AbstractPackageTestCase
{
    protected static function getRequiredServiceProviders(): array
    {
        return [MyPackageServiceProvider::class];
    }
}

5. Write the first test — a container smoke test

// tests/ServiceProviderTest.php
namespace MyVendor\MyPackage\Tests;

use MyVendor\MyPackage\Client;

class ServiceProviderTest extends TestCase
{
    public function testClientIsBoundAsSingleton(): void
    {
        $this->assertTrue($this->app->bound(Client::class));
        $this->assertSame($this->app->make(Client::class), $this->app->make(Client::class));
    }
}

Run it with vendor/bin/phpunit. From here, every test class extends your TestCase and gets a booted Laravel container for free.

Implementation Patterns

Testing bindings, singletons, and aliases. This is the bread and butter of package testing and needs no Laravel app at all:

public function testAliasesAreRegistered(): void
{
    $this->assertSame($this->app->make(Client::class), $this->app->make('my-package.client'));
}

public function testBindingIsNotShared(): void
{
    $this->assertNotSame($this->app->make(Fresh::class), $this->app->make(Fresh::class));
}

Swapping a bound dependency for a Mockery mock. Mockery is already installed and wired up; do not require it separately:

public function testServiceDelegatesToClient(): void
{
    $mock = \Mockery::mock(Client::class);
    $mock->shouldReceive('send')->once()->with('payload')->andReturn(true);

    $this->app->instance(Client::class, $mock);

    $this->assertTrue($this->app->make(Service::class)->dispatch('payload'));
}

AbstractTestCase::tearDown() calls Mockery::close(), so expectations are verified automatically — you never write teardown code for mocks. Also note the "Count mockery assertions" behaviour: mocks and spies register as PHPUnit assertions, so a test that only uses shouldReceive(...)->once() still cannot be reported as risky.

Testing config merging. mergeConfigFrom is one of the most common bugs in packages; test it directly:

public function testConfigIsMerged(): void
{
    $this->assertSame('default-value', $this->app['config']->get('my-package.key'));
}

public function testUserConfigWins(): void
{
    $this->app['config']->set('my-package.key', 'overridden');
    $this->assertSame('overridden', $this->app['config']->get('my-package.key'));
}

Overriding configuration before the provider boots. Use Orchestra Testbench's environment hook rather than setUp — the container has not booted yet when this runs:

protected function defineEnvironment($app): void
{
    $app['config']->set('database.default', 'testing');
    $app['config']->set('my-package.driver', 'array');
}

(Older Testbench used getEnvironmentSetUp($app); the newer hook is defineEnvironment. Match whichever your pinned Testbench version calls.)

Testing artisan commands, routes, and views — all inherited from the Testbench base, no extra glue needed:

public function testCommandRuns(): void
{
    $this->artisan('my-package:sync')->assertExitCode(0);
}

public function testRouteIsRegistered(): void
{
    $this->get('/my-package/ping')->assertOk();
}

Testing facades. Bind the underlying instance, then drive the facade with Mockery:

MyPackage::shouldReceive('ping')->once()->andReturn('pong');
$this->assertSame('pong', $this->app->make('my-package')->ping());

Testing against a full app skeleton. Use AbstractAppTestCase when your package assumes a real Laravel app (bootstrap, config/, routes/). You must return the base path as a static:

class AppTestCase extends AbstractAppTestCase
{
    protected static function getBasePath(): string
    {
        return __DIR__.'/fixtures/app';
    }
}

That fixture directory must look like a minimal laravel/laravel install with a bootstrap/app.php. Everything else (providers, kernel, exception handler) is loaded from it.

CI matrix. Because the package hard-pins versions, mirror that in GitHub Actions: one job per Laravel/PHP pair using composer require "laravel/framework:^11.0" --no-update before install. Do not attempt to composer update a single lock across versions — the package's own conflict rules will fight you.

Gotchas and Tips

The README and changelog disagree about PHPUnit 12. The changelog for 6.3 says "Support PHP 8.5, Laravel 13 and PHPUnit 12", while the README says "PHPUnit 12 is not supported due to volatility across minor releases. We would highly recommend sticking with PHPUnit 12 at this time." — which is almost certainly a stale "12" that should read "11". Treat PHPUnit 11 as the safe choice and only move to 12 if your suite is green on it.

v6.0 made two methods static — this breaks copy-pasted subclasses. AbstractAppTestCase::getBasePath() and AbstractPackageTestCase::getRequiredServiceProviders() are now static, and getRequiredServiceProviders() no longer receives $app as its first argument. If you inherit a test suite from a v5-era package, every subclass override will fatal with a signature mismatch. Fix both the declaration and any call sites.

getBasePath() must point at a real Laravel skeleton. Returning a path without bootstrap/app.php produces a confusing InvalidArgumentException from Testbench's Application::create(). If you only need package-level testing, use AbstractPackageTestCase and skip the fixture entirely.

Never override tearDown() without parent::tearDown(). Mockery::close() lives there. Skipping it means unverified shouldReceive expectations silently pass, which is the worst kind of green build. The same applies to setUp() — call parent::setUp() first, before touching $this->app.

setUp() is too early for config. The application is booted but providers have already run, so $this->app['config']->set(...) in setUp() will not affect a mergeConfigFrom result. Use defineEnvironment/getEnvironmentSetUp for anything a provider reads at boot time.

Database tests need explicit wiring. Testbench gives you an app but not a schema. Set database.default = testing with an in-memory SQLite connection in your environment hook, add use RefreshDatabase, and loadMigrationsFrom(__DIR__.'/../database/migrations') so migrations are picked up from the package rather than database/migrations.

Do not use this package to test a full Laravel application. Laravel ships its own Tests\TestCase that boots bootstrap/app.php for you. TestBench exists for the case where there is no app — a distributable package. Using it in an app means maintaining a fixture skeleton for no benefit.

assertInJson is legacy. It predates the core assertJson/assertJsonFragment helpers and is best avoided in new tests; the underlying Laravel assertions are better maintained and produce clearer diffs.

Debugging tips. --filter on a single test method, dump($this->app->getLoadedProviders()) to see whether your provider was actually registered (a common failure is a typo in getRequiredServiceProviders()), and $this->app->make(...) throwing BindingResolutionException almost always means the provider was never loaded in the first place. $this->withoutExceptionHandling() from Testbench turns swallowed HTTP exceptions into stack traces.

Extension points worth knowing (all inherited from Orchestra Testbench, not this package): getPackageProviders(), getPackageAliases(), defineEnvironment(), defineRoutes(), and getBasePath(). Override getPackageProviders() if you need dynamic provider lists; override the static getRequiredServiceProviders() for the static-declared case.

It is niche, but it is battle-tested. Zero dependents and a tiny surface area, but it is the harness behind roughly every Graham Campbell Laravel package, all of which run large test suites against it on every supported Laravel/PHP combination. The source is under a dozen small files — when in doubt, read it rather than searching for docs, because there are no docs beyond the README.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib