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

Service Mocking Laravel Package

happyr/service-mocking

HappyR Service Mocking is a Laravel testing helper that makes it easy to replace real services with mocks in the container. Swap bindings during functional and integration tests to isolate dependencies and control behavior without manual container wiring or boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

What it actually buys you. One thing: replacing a private, already-wired service inside a booted kernel. $container->set() only works on public services and can't touch a dependency that was constructor-injected into a controller, command, or listener during boot. This bundle swaps the instance in the compiled container so the rest of your object graph keeps its real implementations while one boundary is faked. If that's not the problem you have, Laravel's $this->mock(), $this->instance(), $this->spy(), Http::fake(), and Mail::fake() already cover your seam — stop here.

Install (test-only).

composer require --dev happyr/service-mocking

Then register Happyr\ServiceMocking\HappyrServiceMockingBundle in config/bundles.php. Be honest that this is a test dependency: since the package is require-dev, the bundle never exists in a prod install, but a config/bundles.php entry is unconditional by convention — don't invent env guards you don't need.

Where to look first.

  1. The README quick start — the exact MockFactory method name and whether an allow-list config is required has shifted across versions. Don't guess from a blog post.
  2. src/ — the factory (MockFactory-style class) and the compiler pass. The compiler pass is the real documentation of how services get replaced (public alias, or Container::set() against a test container), and therefore which of your services are actually mockable.
  3. composer.json — the PHPUnit constraint. The mock-building API differs materially between PHPUnit 9, 10, and 11.

First use case: a controller test with a faked HTTP client. Extend KernelTestCase, boot the kernel, then use the package's trait to install a Mockery/createMock double of your client before the request is dispatched:

use App\Service\WeatherClient;
use Happyr\ServiceMocking\MockFactory;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class WeatherControllerTest extends KernelTestCase
{
    public function test_it_renders_forecast(): void
    {
        self::bootKernel();
        $client = $this->createMock(WeatherClient::class);
        $client->method('forecast')->willReturn(['temp' => 21]);

        MockFactory::getContainer(self::$kernel)
            ->replaceService(WeatherClient::class, $client); // name varies by version — check README

        $response = self::$kernel->getContainer()
            ->get('test.client')
            ->request('GET', '/weather');

        self::assertSame(200, $response->getStatusCode());
    }
}

Confirm the real method names against the installed version before you paste that anywhere — the container-replacement entry point is the part of the API most likely to have moved.

Implementation Patterns

Boot the kernel once, swap per test. The trait targets KernelTestCase, so the pattern is always self::bootKernel() in setUp() or at the top of the test, then a container swap, then exercise the app through a handler or test.client. This is the value proposition: real routing, real middleware, real service wiring, one faked edge.

Mock external boundaries, not business logic. The productive target list is payment gateways, weather/currency/maps APIs, outbound mailers that send for real, SMS providers, and webhook receivers. Leave your own domain services real — if you mock those, you've reverted to a unit test with extra boot cost.

Environment-scoped config. The package supports defining mocks per environment. For local dev against a third-party that's slow or rate-limited, this lets you return canned responses without touching application code — the same mechanism as tests, applied to dev. Useful for demos and offline work.

Wrap it, don't sprinkle it. Put the container swap behind one internal helper — e.g. app_test()->fakeService(WeatherClient::class, $double) — so that when the package stalls or you migrate to framework-native fakes, you change one file. This is the single most important decision you'll make about adoption.

For a Laravel-only codebase: port the concept. Do not introduce the Symfony bundle. Build a small MockableServices test trait that records intended bindings, rebinds them to Mockery doubles in setUp(), and restores originals via app()->forgetInstance() in tearDown(). That's roughly the same idea, idiomatic to Illuminate\Container, and avoids carrying a Symfony kernel contract your tests don't otherwise use. Budget one to three days.

Keep the swap visible. A mocked boundary should be one line at the top of the test, named like fakeWeatherClient(21). If a reader can't tell from the test what's faked, the container-swap magic becomes a debugging tax.

Sequencing that pays off. First inventory which tests need this and what they do today. Prototype with Laravel-native helpers. Only if the gap is genuinely "private service constructed deep inside a provider" or "mock at container-compile time" does the package (or a port) earn its slot. Record the outcome in an ADR so the team doesn't re-litigate it every quarter.

Gotchas and Tips

The silent-non-mock failure is the one that will burn you. If the code under test resolves the dependency through new, a static factory, or a service locator that bypasses the container, the mock is never injected and the test passes green against the real service. This is the dominant trust-eroder for container-mocking tools. After swapping, assert something that could only be true of the fake — a specific return value, or a Mockery expectation that must be hit. Never trust a passing test with a mocked service you didn't observe being called.

Private services, aliases, and decorated services. The compiler pass can only replace what's publicly reachable or explicitly declared. Expect "service does not exist" failures at kernel boot, not at the call site. If a dependency is reached only through an alias or a decorator chain, you may need to mock the underlying ID rather than the alias — verify against var_dump($container->getServiceIds()) or debug:container in a real boot rather than guessing.

Final classes and readonly promoted properties resist mocking. Modern codebases use both liberally. Mockery and createMock() can't subclass a final class, and readonly properties can't be re-set post-construction. Either introduce an interface at that boundary, or pick a different seam. Don't waste an afternoon discovering this in a stack trace — grep for final class before you promise the team this boundary is mockable.

PHPUnit major-version skew is the likelier break than Symfony skew. The mock-building API changed between PHPUnit 9, 10, and 11 (createMock behavior around final methods, expectation configuration, willReturnCallback signatures). Pin the package version and pin PHPUnit alongside it. When you bump one, run this suite specifically and read the diff — CI is not protecting you here.

Symfony component lock-step. Laravel 11 ships Symfony 7, Laravel 10 ships 6.4; the package must track both its own Symfony support and your pinned versions. In a Laravel app that's an extra upgrade axis you didn't need. Expect friction at every Symfony major bump and pin accordingly.

Nothing is auto-registered in Laravel. There is no package discovery, no facade, no config/ file. If you try to use it in a Laravel app, the most likely outcome is "nothing happens" — no bundle registration, wrong base test case — and the author concludes the package is broken rather than inapplicable. If you're going down this road, you've already decided to build a wrapper.

Container state leaks between tests. Whatever swap mechanism the package uses, tear it down. Prefer the package's own reset if it exposes one, otherwise call static::ensureKernelShutdown() and rebuild the container per test. A leaked mock bleeding into the next test produces order-dependent failures that are miserable to diagnose.

Debug by inspecting the container, not the test. When a mock isn't being hit, the useful command is bin/console debug:container <id> --env=test (or dumping $container->getServiceIds() inside the test) to see what's actually bound. That single step separates "swap failed" from "swap succeeded but my code resolves the service elsewhere" — the two failure modes look identical from a failing assertion.

Extension point if you outgrow it: the compiler pass. The whole mechanism is a compiler pass plus a trait — small enough to vendor or fork if the maintainer goes quiet (last release 2024-10-08, single-consultancy maintenance, 45 stars, 0 dependents — plan for that). Forking is cheap; the cost is owning a Symfony compiler pass in a Laravel codebase, which the assessment scores poorly for a reason.

Keep the dependency honest. Dev-only means it still lands in composer.lock, Dependabot alerts, and security scans. Pin the version, wrap it, and note in the ADR who owns the wrapper long-term. A 45-star, single-maintainer package is a tactical convenience worth a one-sprint spike — not a platform bet.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle