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

Behat Http Mock Context Laravel Package

macpaw/behat-http-mock-context

A Behat context from MacPaw for mocking HTTP interactions in acceptance tests. Stub external HTTP calls and assert requests/responses so scenarios run reliably without live services or network dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev dependency, pinned to the current patch release (see Gotchas — earlier 3.x builds had a broken config argument name):

composer require --dev macpaw/behat-http-mock-context:^3.0.4

Register the context in your Behat suite. The package exposes a Behat context class (typically MacPaw\Behat\HttpMockContext\HttpMockContext — confirm the FQCN by listing vendor/macpaw/behat-http-mock-context/src/):

# behat.yml
default:
  suites:
    default:
      contexts:
        - MacPaw\Behat\HttpMockContext\HttpMockContext
        - App\Tests\Behat\ApiContext

The context accepts constructor arguments from behat.yml (that is the package's entire configuration surface — see Gotchas). Check the context's __construct signature before adding any, since v3.0.4 exists precisely to fix a mismatch between the documented and actual argument name:

contexts:
  - MacPaw\Behat\HttpMockContext\HttpMockContext:
      someArgument: someValue   # verify exact key against __construct

The first place to look after install is the context class itself and its *.feature fixtures (usually under tests/ or features/) — that is the source of truth for the exact step phrasings. The package is small enough that reading the context top-to-bottom (usually 200–400 lines) will tell you everything it does in a few minutes.

Your first use case: a scenario that would normally call an external API (payment provider, auth service, CRM). Stub the response in the Given step so the assertion in the Then step runs against deterministic JSON instead of a live endpoint:

Scenario: Customer sees their subscription
  Given a mocked "GET" response for "https://api.billing.test/v1/subscriptions/42"
    """
    {"id": 42, "plan": "pro", "status": "active"}
    """
  When I request "GET /api/me/subscription"
  Then the response should contain "plan": "pro"

The critical prerequisite is that your application's HTTP client must resolve to the same mock instance the context creates. See Implementation Patterns — this is where most adoption friction lives.

Implementation Patterns

Wire the mock client into your app's container. The context produces a mock HTTP client (typically a PSR-18 Http\Mock\Client or a Guzzle MockHandler). Your application kernel must be configured to use that instance when the test environment boots. The canonical pattern is:

  1. Instantiate a shared mock client in a bootstrap file or in a BeforeSuite hook.
  2. Bind it as a service in your test container (Symfony: services_test.yaml; Laravel: a testing service provider that rebinds the client binding).
  3. Have HttpMockContext enqueue responses onto that same shared instance.

If your app constructs HTTP clients ad hoc (e.g., new Client() inside a service), you cannot mock them cleanly. Route all outbound calls through a single injected interface (Psr\Http\Client\ClientInterface, or a Guzzle client service) so the mock can be swapped at the container level.

Sequence responses for retry / pagination flows. Queue multiple responses and rely on FIFO order to simulate a 429-then-200, or page-by-page pagination:

Given a mocked "GET" response for "/v1/items?page=1" with status 200
Given a mocked "GET" response for "/v1/items?page=2" with status 200

Assert on the queue being drained in your AfterScenario hook so a test that never triggered the call fails loudly rather than silently passing on a leftover stub.

Match on method + URL, not just URL. Third-party APIs commonly hit the same path with different verbs. Always specify the HTTP method in the step; if the package supports it, also match on the request body or a header (e.g., Idempotency-Key) to differentiate retries.

Cross-context state sharing. The mock context and your API context are separate Behat contexts. Expose the mock client (or the mock context itself) via a shared trait or getMockClient() accessor, and have your API context fetch it in BeforeScenario to inject into the kernel. Behat contexts are plain objects — the package cannot automatically reach into another context's dependencies for you.

Symfony + HTTPlug users: register Http\Mock\Client as the httplug.client service in the test environment and point HttpMockContext at that service ID. Laravel users: bind a mock Illuminate\Http\Client\Factory in a TestingServiceProvider and enqueue responses per request URL.

Gotchas and Tips

  • Upgrade to v3.0.4 if you are on any earlier 3.x. The 3.0.4 patch fixes an argument name in config — i.e. the constructor/argument key used in behat.yml did not match what the package actually read. Symptom on older 3.x: you pass a value in behat.yml and it is silently ignored (Behat named arguments are positional-by-name; a typo'd key either throws or leaves the default in place). After upgrading, re-read __construct and correct any keys you copied from the README.

  • The mock client must be a singleton. If your container creates a fresh client per request (e.g., a factory binding), the responses queued by the context land on a different instance than the one your code invokes. Symptom: "unexpected request" errors and an always-empty mock queue. Fix: bind the mock as a shared: true service.

  • URL normalization matters. https://api.test/v1/users and https://api.test/v1/users? and /v1/users are treated differently by strict matchers. Decide early whether your steps take absolute URLs or path-only fragments, and write a small helper to normalize. Trailing slashes and query-string ordering are the usual culprits.

  • Body encoding bites. If the package lets you provide a PHP array for the body, verify the Content-Type header it sends — you want application/json for JSON assertions and application/x-www-form-urlencoded for form assertions. Sending a raw string when the client expects an array (or vice versa) usually produces a confusing exception from deep inside Guzzle/PSR-7.

  • Debugging unmatched requests. When a scenario fails with "no mocked response" or "request not matched", dump the enqueued queue and the received request in a BeforeStep/AfterStep hook. Most PSR-18 mock clients expose getRequests() and getLastRequest() — log method, URI, headers and body. This turns a 30-minute debugging session into a 30-second one.

  • Configuration is limited to behat.yml context arguments; extension happens in the context class. There is no YAML config file of the package's own and no DI extension — the knobs are the constructor arguments declared in behat.yml (the surface that 3.0.4 patched), and the intended customization path is subclassing HttpMockContext (or overriding a small trait) for project-specific step phrasings like Given the CRM returns error 500 for customer :id. Subclass, add steps, and register your context instead of the vendor one in behat.yml.

  • Maintenance risk is real. Four stars, zero dependents, a handful of patch releases and a single maintainer-backed history means this is effectively MacPaw's internal tooling published publicly. The 3.0.4 fix landing as a patch rather than a minor suggests the project tracks semantic versioning loosely for config. Treat it as a readable reference implementation: pin the exact version (^3.0.4 at minimum, or a locked patch), and keep a thin internal wrapper around it (a ProjectHttpMockContext that delegates) so you can swap it for php-http/mock-client + a dozen custom steps if it is ever abandoned. It is small enough to fork in an afternoon.

  • Do not let mocks leak into AfterScenario order. If a scenario fails before draining the queue, the leftover stubs stay on the shared client and poison the next scenario. Reset the mock client in BeforeScenario (or whatever the package's reset method is) unconditionally.

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