pkly/phpunit-service-create-trait
A PHPUnit 12+ trait (PHP 8.4+) that builds real service instances with auto-mocked constructor and #[Required] dependencies. Dependencies stay as invisible stubs until fetched, so you only get mock verification where you ask for it.
$this->createRealMockedServiceInstance(OrderService::class) — and keeps tests compiling when a constructor gains or reorders a dependency. The PM-relevant decision is not "adopt a package," it's "do we keep paying a per-refactor test-maintenance tax, or spend a day eliminating it?" Either way the work is small; the decision is cheap to make and cheap to reverse.getMockedService() registers an expectation so it's verified; getStubbedService() returns a plain stub. That is an opinionated answer to a real quality problem: teams either drown in "no expectations configured" noise (and learn to ignore PHPUnit output, which is how real failures get missed) or over-mock every dependency and produce brittle, assert-nothing tests. Adopting this trait is partly adopting a testing philosophy, so it's a fit question, not just a utility question.OrderService → repository + payment client + logger), services that also use Symfony-style #[Required] setter injection, and legacy services whose constructors are wide and volatile.Http::fake()/Queue::fake(). For those, this trait is irrelevant — don't let it into a "test quality initiative" narrative where it doesn't belong.Adopt (as a pilot, scoped to one module) if:
require-dev, MIT, and trivially removable (tests fail loudly if it breaks, and nothing ships to production).Look elsewhere if:
mock()/fake() helpers and its function-first style cover much of the same ground with a different ergonomics story; mixing the two conventions in one repo is a net loss.RefreshDatabase, fakes, and contract tests.Before you commit, run a short technical spike (half a day, not a sprint) to confirm behavior on your actual code: a service with a scalar-without-default, one with an enum dependency, one with two dependencies of the same type (getMockedService(BasicService::class, 'first')), one nullable dependency, and one with #[Required] setters. That list is exactly where the README says friction lives, so it's the right acceptance test — and the answer is more informative than any star count.
For executives — frame it as a small, reversible productivity line item, not a platform bet.
Our unit tests for service classes spend real engineering time on setup that has nothing to do with what's being tested: every time a service's constructor changes, we rewrite the mock plumbing in its test file. This trait replaces that with a single line —
$this->createRealMockedServiceInstance(OrderService::class) — which builds the real service with every dependency automatically filled in. Two honest caveats: it requires our PHP 8.4 / PHPUnit 12 toolchain, so it rides along with an upgrade we already need; and it's a tiny open-source utility (MIT, one maintainer, no dependents), used only in tests and never in production code. I'm not going to quote a made-up productivity percentage. I'd rather spend a day piloting it in one module and bring back measured numbers — setup lines removed per test and time-to-green on a real refactor. If it doesn't pay for itself, removing it is a one-line change.
For engineering — the substance is the mocking semantics, not the auto-wiring.
Auto-wiring is reflection over the constructor plus #[Required] setters; anyone can write it. What's worth adopting is the ownership model: dependencies are created as doubles PHPUnit doesn't know about, so unfetched ones never trigger "no expectations configured" noise. You opt a dependency in with getMockedService() (registered → expects() is verified) or getStubbedService() (plain stub, for return values only). That means strict-mode output stays meaningful, and tests assert on the couple of collaborators that matter instead of stubbing everything reflexively. The service is constructed immediately, so you can declare expectations before or after you exercise it — no ordering traps. Duplicate dependency types get distinct doubles and need a parameter name (getMockedService(BasicService::class, 'first')); scalars without defaults and enums must be passed explicitly; nullable deps still get a double, so pass null if that's the intent. createRealPartialMockedServiceInstance covers partial mocks, though those follow normal PHPUnit expectation rules.
Two constraints to design around: #[Required] is Symfony's attribute, so the setter-injection half of the feature is only worth much if our services actually use it — Laravel's constructor injection is where the value concentrates. And this is a unit-test-only tool; it does nothing for HTTP, queue, or database-backed feature tests, which should keep using Http::fake(), Queue::fake(), and RefreshDatabase. Proposed next step: one module pilot with the edge-case list above as acceptance criteria, then a go/no-go based on measured maintenance impact — not on the package's star count, which is zero, and not on a vendor's benchmark, which doesn't exist.
How can I help you explore Laravel packages today?