graham-campbell/testbench-core
Laravel TestBench Core by Graham Campbell provides reusable testing functionality for Laravel packages and applications, supporting PHPUnit 9-12 and Laravel 8-13 on PHP 7.4-8.5.
Architecture fit. TestBench Core is a dev-only, dependency-injected test harness, not a runtime library. It ships a small set of PHPUnit traits — FacadeTrait, ServiceProviderTrait, MockeryTrait, plus helper assertions such as assertArraySubset — that let a test suite boot a Laravel application container and exercise facades and service providers without each package reimplementing that plumbing. There are no service providers to register and no configuration, which is the most important architectural fact for a TPM: the package has zero production surface area. It cannot affect runtime behavior, request latency, container bootstrapping in production, or deployment artifacts. Its blast radius is entirely contained to composer.json require-dev and the test job in CI.
The architectural fit is therefore conditional on our code being package-shaped. The value concentrates where service providers and facades are the public contract — publishable Laravel packages, SDK-style shared libraries, modular domain packages, multi-tenant plugins. In a conventional single-application monolith with feature/HTTP tests, this package solves a problem we do not have. That is a portfolio-shape question, not a code question, and it should be answered before any evaluation of the library itself.
Integration feasibility. High, with a low ceiling on effort. composer require "graham-campbell/testbench-core:^4.3" --dev is the entire install. Adoption is a code-level refactor of existing base TestCase classes to extend or compose the provided traits — mechanical work, well-bounded, and reversible by deleting the dependency and inlining the traits. The README's usage documentation is deliberately thin ("see an example of implementation in pretty much all of my Laravel packages"), so discoverability depends on reading the maintainer's other repositories. Plan for one engineer to spend a short spike producing an internal usage note; that note, not the upstream README, becomes our onboarding artifact. Feasibility is not the risk here — the risk is a team silently reimplementing the traits because the docs gave them nothing to copy.
Technical risk. Four items, in descending order of concern:
FacadeTrait::getFacadeAccessor/getFacadeClass/getFacadeRoot/getServiceProviderClass and ServiceProviderTrait::getServiceProviderClass static, and added property/return types throughout. That is a legitimate major bump, but it is exactly the kind of change that breaks every adopting test suite at once across a monorepo. Budget for it: a major TestBench release is a monorepo-wide PR, not a composer update.Key questions.
orchestra/testbench or the maintainer's other packages? If so, the decision is already made and this becomes a version-pinning exercise.Stack fit. Adopt only if PHP 7.4–8.5, PHPUnit 9–12, and Laravel 8–13 covers our current matrix. It does for most contemporary stacks, so the binding constraint is PHPUnit, not Laravel. Confirm our PHPUnit version first — if any team is on PHPUnit 13, this package is a non-starter until upstream ships support, and that is a hard stop rather than a workaround.
Migration path. Three steps, in order:
TestCase base classes, hand-rolled facade assertions, manual $this->app container bootstrapping, and duplicated Mockery setup. This inventory is the business case — it quantifies the drift the package would consolidate.TestCase per package tier that consumes the traits, so application teams never touch the traits directly. This keeps the dependency behind one seam and makes the inlining fallback a one-file change.Compatibility. Pin the major (^4.3) and let Composer resolve minors; this package has shipped frequent, small, patch-level fixes (v4.2.1 corrected a MockeryTrait deprecation warning, v4.3.0 added PHP 8.5 / PHPUnit 12 / Laravel 13). Add it to whatever dependency-update automation we run, and treat its release notes as a leading indicator for our next Laravel upgrade — the changelog is effectively a public compatibility ledger, which is unusually useful for planning. Do not float the constraint; a major bump here breaks every adopting suite simultaneously.
Sequencing. Tie adoption to the next scheduled Laravel or PHPUnit upgrade rather than running it as standalone work. Rationale: the package's version matrix already gates that upgrade, so the two changes share a single CI validation cycle and a single rollback point. Adopting it mid-cycle, separately, doubles the number of times we exercise the same risk for no additional benefit.
Maintenance. Ongoing cost is close to nil in the steady state — no runtime, no configuration, no service providers, no migrations. The cost is periodic and lumpy: whenever PHP, PHPUnit, or Laravel moves, this dependency must be certified before our CI will pass, and when it moves a major, our adopting suites need a coordinated update. Model it as a small annual maintenance line item plus a known monorepo-wide event on major releases, not as a recurring per-sprint cost.
Support. Community-maintained with a formal security policy, a documented release process, a changelog going back to v1.0, and a Tidelift commercial channel for enterprise support. For an internal test dependency, community cadence is usually defensible; escalate to Tidelift only if a blocked CI pipeline has direct revenue or release-commitment consequences. Note the support relationship is inverted from production dependencies: a production library failing costs us customers, a test library failing costs us engineering hours. Size the support decision against the latter.
Scaling. Scales well across a package portfolio — the marginal cost of the tenth adopting package is roughly zero, and the package's real value is consistency: one set of facade/service-provider/Mockery conventions across every package, which reduces review friction, copy-paste drift, and ramp time for engineers rotating onto package work. It does not scale down: for a team with one or two packages, hand-rolling the helpers is cheaper than the version-constraint bookkeeping. Frame adoption as a standardization lever, not a velocity lever — do not promise faster tests.
Failure modes. (a) PHPUnit 13 gap — upstream support lags our pin, blocking an upgrade; mitigation is holding PHPUnit 12, which the maintainer explicitly recommends. (b) Major-version BC break — the v4.0 static-method change is the template; mitigation is the single internal base TestCase seam. (c) Single-maintainer abandonment — mitigation is the inlining fallback, which is genuinely cheap given the package's small surface. (d) Silent reimplementation — engineers ignore the traits and roll their own because upstream docs are by-example only; mitigation is the internal usage note. (e) Transitive constraint conflict — the package's PHPUnit/PHP/Laravel constraints collide with another dev dependency in a large monorepo; mitigation is per-package rather than root-level pinning during the pilot.
Ramp-up. Near-zero for anyone who already writes Laravel package tests, since the traits read like the conventional pattern. The friction is entirely documentation-shaped: the README says "you may see an example of implementation in pretty much all of my Laravel packages," which is not a learning path most engineers will follow unprompted. Budget roughly a half-day for one engineer to produce an internal reference — the trait surface is small enough that this is a bounded task, and it converts the package's weakest attribute (docs-by-example) into a durable internal asset that survives even if upstream goes quiet.
Bottom line for the decision review. Adopt as a require-dev dependency behind a single internal base TestCase, sequenced with the next Laravel/PHPUnit upgrade, if we are shipping multiple Laravel packages. Skip it if our roadmap is a single application monolith — the compatibility coupling is real and the payoff is zero in that shape. Either way, record the PHPUnit 12 pin as a named constraint in upgrade planning so the next Laravel major is scoped with this dependency in view rather than discovered mid-sprint.
How can I help you explore Laravel packages today?