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

Functional Test Bundle Laravel Package

speicher210/functional-test-bundle

Symfony bundle for functional REST endpoint tests: sets up test databases, loads Doctrine fixtures, mocks even private DI services, and validates responses via JSON snapshots using php-matcher, with optional automatic expected-file updates.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install as a dev dependency.

    composer require --dev speicher210/functional-test-bundle
    
  2. Register the bundle for dev and test only in config/bundles.php:

    Speicher210\FunctionalTestBundle\Speicher210FunctionalTestBundle::class => ['dev' => true, 'test' => true],
    
  3. Create a PHPUnit bootstrap that builds the kernel once. The bundle's bootstrap expects a $kernel variable in the global scope:

    // tests/bootstrap.php
    require dirname(__DIR__) . '/config/bootstrap.php';           // must assign $kernel
    require_once dirname(__DIR__) . '/vendor/speicher210/functional-test-bundle/src/Test/bootstrap.php';
    

    Point phpunit.xml at it (bootstrap="tests/bootstrap.php") and set APP_ENV=test / KERNEL_CLASS in <php>.

  4. First use case: a smoke test on a REST endpoint. Extend RestControllerWebTestCase and let the first run generate the snapshot:

    use Speicher210\FunctionalTestBundle\Test\RestControllerWebTestCase;
    use Symfony\Component\HttpFoundation\Request;
    
    final class UserEndpointTest extends RestControllerWebTestCase
    {
        public function testReturns404WhenUserIsMissing(): void
        {
            $this->assertRestRequestReturns404('/api/user/1', Request::METHOD_GET);
        }
    
        public function testReturnsUserData(): void
        {
            $this->assertRestGetPath('/api/user/1');
        }
    }
    
  5. Where to look first in the source. src/Test/RestControllerWebTestCase.php (the assertion API you'll live in), src/Test/WebTestCase.php (kernel/container/fixture plumbing), and the snapshot assertion class under src/Assert/ (how files are named, stored, and compared). With 5 stars and 0 dependents, the source is the documentation — budget an hour to read those three files before writing tests.

Implementation Patterns

Snapshot-driven body assertions, status/header assertions for the contract. Use the dedicated assertRest* helpers for status codes and let snapshots cover the JSON body. Don't snapshot the whole response for endpoints with volatile fields — that turns every test into a changelog.

Golden-file workflow. First run writes the snapshot; commit it; every subsequent run diffs against it. Treat snapshot diffs in code review exactly like fixture diffs: a changed snapshot is either a deliberate API change or a regression. Keep snapshots in a predictable directory next to the test (__snapshots__) so ownership is obvious.

CRUD chains. For write endpoints, drive the flow through the client and capture identifiers rather than hardcoding them:

$client = static::createClient();
$client->request(Request::METHOD_POST, '/api/users', [], [], [], json_encode(['email' => '[email protected]']));
$id = json_decode($client->getResponse()->getContent(), true)['id'];

$this->assertRestGetPath('/api/users/' . $id);

Fixtures + DB setup. Keep the base class responsible for booting the kernel and schema; load fixtures via DoctrineFixturesBundle in setUp() or a shared trait. Use a fresh database per run (SQLite in-memory or a truncated MySQL schema) and prefer deterministic fixture data — fixed IDs, frozen clock — because snapshots encode whatever the DB returns.

Mocking services, including private ones. This is the bundle's real differentiator: it lets you swap a service in the compiled container before the request is handled, which plain WebTestCase cannot do without a compiler pass. The pattern:

$mock = $this->createMock(BillingClient::class);
$mock->method('charge')->willReturn(new ChargeResult('ok'));

$this->mockServiceInContainer(BillingClient::class, $mock); // helper from the bundle's test case
$client->request(Request::METHOD_POST, '/api/orders', ...);

Verify the exact method name against src/Test/ before adopting — it's the one API worth pinning in a base class of your own so a future version bump is a one-line change.

One test case per scenario, snapshots named per case. Use data providers for variations and give each data set its own snapshot; a single test asserting five payloads produces unreadable diffs.

Integration tips. Wrap the bundle's base class in your own ApiTestCase that sets your default headers (auth token, Accept: application/json), your fixture loader, and your snapshot path. Every project ends up with this layer; write it on day one.

Gotchas and Tips

  • A missing snapshot can pass silently. Most implementations write the file when absent and only fail on mismatch — CI on a clean checkout with an uncommitted snapshot is green. Add a CI step that runs the suite twice or asserts git status --porcelain is clean afterward.
  • Volatile fields wreck snapshots. IDs, createdAt, ETags, and trace IDs change per run. Normalize them out (replace with "__ID__") or freeze time/IDs in fixtures. This is the single biggest source of churn in this style of testing.
  • Kernel reboot clears your mocks. Symfony reboots the kernel between requests by default; a mocked service registered before request #2 may be gone. Call $client->disableReboot() when a mock must survive multiple requests — and remember it also leaks state between tests.
  • $kernel scope trap. The bundle's bootstrap relies on your config/bootstrap.php putting the kernel in a global $kernel. If your project's bootstrap returns the kernel instead (common in newer skeletons), the bundle bootstrap won't see it. Fix by assigning explicitly.
  • Private-service mocking versus static::getContainer(). Raw Symfony test containers only expose public services. self::$container is deprecated/removed across versions — prefer static::getContainer() and let the bundle's helper handle private lookups. Expect friction if you're on Symfony 4.x/5.0-era code with an older bundle release.
  • PHPUnit version drift. Older test-base classes may use annotations and void-less setUp() signatures. Check compatibility with PHPUnit 10/11 before committing; if the base class blocks you, pin PHPUnit rather than forking the bundle.
  • Database differences bite snapshots. SQLite returns "1" where MySQL returns 1 for integer-ish columns; JSON column ordering can differ. Run functional tests on the same engine as production if snapshots must be portable.
  • Debugging routine. When a snapshot mismatch is opaque, dump $client->getResponse()->getContent() and the status/headers before the assertion fires; run with --filter on a single test; check var/log/test.log. Diff the generated file against the committed one directly — the failure message rarely shows the full picture.
  • Extension points. The base case is designed to be subclassed: override the snapshot directory, the snapshot name resolver, or the response normalizer rather than editing vendor code. That's also your escape hatch if the project stalls — a 5-star, 0-dependent package is a maintenance bet, so keep your customizations in one ApiTestCase you could re-point at spatie/phpunit-snapshot-assertions + plain WebTestCase if needed.
  • Don't use it for unit tests. Everything here boots a kernel and hits the container; the cost per test is seconds, not milliseconds. Reserve it for endpoint-level contracts.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource