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

Testing Laravel Package

spiral/testing

Spiral Testing SDK provides a TestCase and custom TestApp implementation for testing Spiral Framework applications and packages. Configure bootloaders and root directory to run tests without building extra app classes. Requires PHP 8.1+ and Spiral 3.15+.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the SDK as a dev dependency:

composer require --dev spiral/testing

Requirements are strict: PHP 8.1+ and Spiral Framework 3.15+. Note that version 3.x of the package targets PHPUnit 13, so if you are still on PHPUnit 10/11, stay on the 2.x line.

The first thing to do is decide whether you are testing an application or a package.

For an application, create tests/TestCase.php extending the SDK's TestCase and point it at your project root:

namespace Tests;

abstract class TestCase extends \Spiral\Testing\TestCase
{
    public function rootDirectory(): string
    {
        return __DIR__.'/..';
    }
}

For a package, you have no application or bootloaders, so you must supply both. The SDK ships a custom TestApp implementation for exactly this case:

tests
  - app
    - config
      - my-config.php
  - src
    - TestCase.php
    - MyFirstTestCase.php
namespace MyPackage\Tests;

abstract class TestCase extends \Spiral\Testing\TestCase
{
    public function rootDirectory(): string
    {
        return __DIR__.'/../';
    }

    public function defineBootloaders(): array
    {
        return [
            \MyPackage\Bootloaders\PackageBootloader::class,
        ];
    }
}

Your first real test should exercise an HTTP route through fakeHttp():

final class HomeControllerTest extends TestCase
{
    public function testIndex(): void
    {
        $this->fakeHttp()
            ->get('/')
            ->assertOk();
    }
}

If that passes, the container booted, routing resolved, and the response pipeline ran end to end. Everything else builds on this same pattern.

Implementation Patterns

HTTP testing via fakeHttp(). This is the workhorse of the package. It exposes get, post, patch, put, delete, plus *Json variants for JSON payloads. Assertions live on the returned TestResponse (assertOk, assertStatus, assertRedirect, etc.), and assertStatus includes the response body in the failure message, which saves a lot of debugging time.

$this->fakeHttp()
    ->postJson('/api/users', ['name' => 'John'])
    ->assertStatus(201)
    ->assertJsonPath('data.name', 'John');

Middleware manipulation. When you need to bypass auth or add test-specific middleware:

$this->fakeHttp()
    ->withoutMiddleware(AuthMiddleware::class)
    ->get('/dashboard');

withMiddleware() was added alongside the fix for scoped middleware; both require Spiral 3.15+.

Custom requests and streams. Since 2.10.0, createRequest(), createJsonRequest(), and handleRequest() are public, so you can assemble requests manually when the fluent helpers don't fit. Streams can also be sent via fake HTTP requests, which is useful for upload endpoints.

Reading JSON bodies. getJsonParsedBody() on the fake HTTP helper returns the decoded payload when you need to inspect data without an assertion.

Faking the event dispatcher. Always resolve it in setUp() after calling parent::setUp():

protected function setUp(): void
{
    parent::setUp();
    $this->eventDispatcher = $this->fakeEventDispatcher();
}

Then assert on it with the full suite: assertListening(), assertDispatched(), assertDispatchedTimes(), assertNotDispatched(), assertNothingDispatched(), dispatched() (returns matching events), and hasDispatched(). assertDispatched accepts a truth-test callback for filtering:

$this->eventDispatcher->assertDispatched(
    UserRegistered::class,
    static fn (UserRegistered $e): bool => $e->userId === 1,
);

Config fragment assertions. assertConfigHasFragments() checks nested config values without comparing whole files:

$this->assertConfigHasFragments('http', [
    'headers' => ['Content-Type' => 'text/html; charset=UTF-8'],
]);

This is a much more maintainable pattern than full-array equality when you only care about one branch.

Container assertions. assertContainerBound() uses Container->hasBinding() internally as of 2.12.0, so it correctly distinguishes bound-but-not-yet-resolved services. Combine with singleton assertions when validating bootloader behavior in packages.

Scoped tests. Wrap tests that exercise scoped services using the #[TestScope] attribute:

use Spiral\Testing\Attribute\TestScope;

#[TestScope('http')]
public function testIndexAction(): void
{
    $this->fakeHttp()->get('/')->assertOk();
}

This is the idiomatic way to pin a test to a named scope when your code depends on scope-local singletons.

Package testing workflow. For packages, config lives under tests/app/config/, and defineBootloaders() lists the bootloaders to boot — effectively constructing a minimal application per test suite. Keep bootloaders minimal per TestCase subclass so each suite can isolate its feature; you can have multiple TestCase bases for different feature groups.

Console and scaffolder assertions. The SDK includes assertions for console commands and for scaffolder output (added in 2.3.0, with line-ending normalization in 3.1.2). Interactive commands are automatically run in non-interactive mode (2.8.2), so you don't need to pipe input manually.

Queue assertions. The SDK exposes fake queue helpers and validates payload types, so you can assert jobs were pushed without running a worker.

Gotchas and Tips

  • rootDirectory() is load-bearing. It must point at a directory containing app/config (package layout) or your application root. Wrong paths produce confusing "config not found" errors that look like bootloader failures.
  • defineBootloaders() replaces defaults, not appends. For packages, if a bootloader depends on another, list it explicitly. Forgetting a framework bootloader is the most common cause of "service not found" in package tests.
  • Event dispatcher must be faked after parent::setUp(). Calling fakeEventDispatcher() before the container is initialized will fail or bind the wrong instance.
  • Finalizers run after each HTTP request (2.12.1), and 2.12.2 added handling for container proxy scope exceptions during finalizer calls. If you see scope exceptions at test teardown, upgrade — do not try to work around it.
  • Files binding is restored post-test (2.3.1). If you manually swap FilesInterface for a fake, the SDK restores the original binding after the test; don't rely on a fake leaking between tests.
  • HTTP tests always run in the http scope (2.8.1). If a service you are asserting on lives in a different scope, use #[TestScope] or a scope-aware assertion to avoid false negatives.
  • assertStatus is verbose by design. It dumps the response body on failure. That's a feature, not a bug — remove competing dump() calls before concluding the output is noisy.
  • Singleton rebinding had historical issues (fixed in 2.4.1). If you're pinning an older version, container assertions around singletons may be unreliable. Pin >=2.12 or the current 3.x line.
  • Config modifications via attributes have had bugs (fixed 2.6.4). If attribute-driven config overrides silently don't apply, check the version.
  • Line endings in console/scaffolder assertions were normalized in 3.1.2. Comparisons against Windows-style output on older versions will fail for no visible reason.
  • PHPUnit version coupling is unforgiving. 3.0.0 bumps PHPUnit to 13.0.5; the 2.x line had a PHPUnit 10.5.46 conflict fix in 2.9.1. Match your PHPUnit constraint to the SDK major before upgrading either.
  • withoutMiddleware() with scoped middleware was broken before 2.9.0. If bypassing auth silently fails, that's the culprit.
  • assertConfigHasFragments is partial-match. It validates the specified subtree only — it will not catch extra keys elsewhere in the config. Don't use it as a schema validator.
  • Extension point: keep a single package TestCase base with defineBootloaders() returning the union of required bootloaders, and override per-suite only when isolation matters. Over-splitting the base class fragments rootDirectory() logic and creates hard-to-trace config drift.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels