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

Tested Routes Checker Bundle Laravel Package

tiime/tested-routes-checker-bundle

A Symfony bundle that checks which application routes are covered by tests, flagging untested endpoints so teams can catch coverage gaps early and keep route testing visible in CI.

View on GitHub
Deep Wiki
Context7

Getting Started

tiime/tested-routes-checker-bundle is a test-tooling bundle: it records which Symfony routes are actually hit while your test suite runs, then lets you assert that every registered route was exercised. Treat it like a coverage report for your routing table.

  1. Install as a dev dependency — this should never ship to production:

    composer require --dev tiime/tested-routes-checker-bundle
    
  2. Register the bundle only where tests run. Since it hooks into the kernel and request lifecycle, scope it to test (and optionally dev) in config/bundles.php:

    return [
        // ...
        Tiime\TestedRoutesCheckerBundle\TestedRoutesCheckerBundle::class => ['test' => true],
    ];
    
  3. Look at the bundle's own README/config first. The package is pre-1.0 and small; the authoritative surface area is the bundle class, its DependencyInjection/Configuration.php, and the console command it exposes (usually something under a tested-routes: namespace). Read the Configuration.php before guessing config keys — that file tells you exactly what's tunable (storage path, exclusion patterns, output format).

  4. First use case: run your functional/integration tests once so the checker records the routes it saw, then run the bundle's "check" command. It will diff the recorded set against the router's RouteCollection and report anything untouched:

    bin/phpunit
    bin/console tested-routes:check
    

    A green run means "every route in config/routes*.yaml / attributes was requested at least once." A red run gives you the list of orphans — exactly the routes that have no test hitting them.

Start with one small controller so you can see the tracking file appear and understand its shape before wiring it into CI.

Implementation Patterns

CI as a gate. The most valuable placement is a dedicated CI step after your test suite:

- run: bin/phpunit
- run: bin/console tested-routes:check

Because it consumes the state written during the tests, it must run after the suite in the same job/workspace — don't split them across runners unless you persist the tracking artifact between them.

Reset before every run. Always start from a clean tracking state so stale route names from a previous suite don't mask a regression. A rm -f var/tested-routes* (or the bundle's own reset flag) at the top of the job, or in a PHPUnit bootstrap/setUpBeforeClass, is the reliable pattern.

Exclude the boring routes explicitly, not implicitly. Routes for health checks (/healthz), webhooks (Stripe, GitHub), the profiler, error pages, and assets served through a controller typically shouldn't count. Put them in the bundle's exclusion config rather than writing pointless smoke tests:

# config/packages/test/tested_routes_checker.yaml
tested_routes_checker:
    excluded_routes:
        - '_profiler*'
        - '_wdt'
        - 'health_check'
        - 'webhook_*'

Pair it with a "smoke test" controller test. For routes that are hard to cover end-to-end (authenticated admin pages, form submissions), write one parameterized functional test that logs in a fixture user and GETs every listed route, asserting < 500. That both satisfies the checker and catches template/runtime explosions. This is often cheaper than writing a bespoke test per route.

Track per-suite when you have multiple test layers. If you run unit tests (which never boot the kernel) and functional tests separately, only the functional run produces meaningful tracking data. Wire the checker into the functional suite only, or you'll get confusing "partially tested" reports.

Combine with a coverage threshold. The checker answers "was this route reached?" — not "was the controller code inside it covered?" Run it alongside Pest/PHPUnit coverage (--coverage-text --min=80) so a route being hit by a shallow 404-producing test doesn't give false comfort.

Pre-commit / local feedback. For a fast inner loop, add the check to a composer test script:

"scripts": {
    "test": [
        "@php bin/phpunit",
        "@php bin/console tested-routes:check"
    ]
}

That way developers see orphaned routes before pushing rather than in CI.

Gotchas and Tips

State file persistence is the #1 source of confusing results. If the tracking file lives in var/ and isn't cleared, a passing local run can make CI green even after you deleted the test that covered a route. Add the tracking file to .gitignore, clear it in the bootstrap, and treat "file exists from yesterday" as a bug in your setup.

Subrequests and fragments inflate the record. Symfony fires kernel.request for ESI/fragment/render(controller(...)) subrequests, and your listener will see those too. Route names usually dedupe this correctly, but if the checker keys off paths or request URIs you'll get counts that don't match reality. Prefer route names, and dedupe in a Set.

Redirects count as "tested" — sometimes misleadingly. A 302 from /old to /new marks /old as covered, but the target controller may never run if the client doesn't follow. If your test client has followRedirects(false) (the default in Symfony's KernelBrowser), assert the redirect and follow it, or you'll have "covered" routes that execute one line of code.

Route parameters are not the test. /orders/{id} records as orders_show regardless of whether you tested id=1 or id=999. The checker gives you breadth, not depth — pair it with data-provider tests for boundary values.

Routes are matched by name, and names can be autogenerated. Attribute routes without an explicit name: get names like app_controller_method. Refactoring a controller method silently renames the route, so the checker may report the old name as missing and the new one as untested across a single commit. Name your routes explicitly (#[Route('/orders/{id}', name: 'orders_show')]) — it makes the checker's diffs readable and stable.

Console commands are invisible to it. Anything registered as a console command, or reached only via Messenger workers/CRON, is outside kernel.request and will always look "untested." Don't try to force those into the checker; use CommandTester / Messenger assertions for those separately.

404s and unmatched requests may be recorded. If your listener logs every request rather than only those where $request->attributes->get('_route') is set, a deliberate 404 test can mark an unrelated path as covered. Guard with a null check on _route and skip when it's null.

Environment and cache warmth matter. The checker compares against the router as compiled for the current environment. Run the check with the same APP_ENV (usually test) you used for the suite; running it under dev picks up dev-only routes (profiler, _errors/preview, etc.) and produces phantom misses. Run bin/console cache:clear --env=test if you've just added routes and see stale entries.

Debugging the tracker itself. When the report is surprising, don't guess — dump what was collected. The tracker service is a normal Symfony service in the container, so you can fetch it in a test (self::getContainer()->get(...)) and var_dump the recorded set, or tail -f the tracking file between tests. Knowing the file format takes the mystery out of the diff.

Parallel test runs need care (Paratest, --processes). Multiple workers writing to the same JSON/file will corrupt or lose entries. Either disable parallelism for the functional suite when using this bundle, configure a per-worker tracking file and merge them, or run the checker against a serial run in CI.

It is a small, low-traffic package — pin it. With 1 star and 0 dependents, expect occasional upstream churn in config keys or class names between releases. Pin a range in composer.json (^1.0 or the specific version you tested), and read the CHANGELOG on upgrade. Because it's dev-only, the blast radius of a break is small — but a silent config-key change can make the checker pass vacuously rather than fail loudly.

Extension points. If you need custom behaviour (e.g., writing the results to a JUnit XML report, or emitting a Slack summary), the bundle will most likely expose a decorated storage/reporter service rather than a hard-coded command. Override the relevant service in config/services_test.yaml with decorates: — that's the idiomatic Symfony way and avoids forking the bundle.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk