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

Paypal Fake Laravel Package

puntodev/paypal-fake

A lightweight PayPal fake for Laravel testing. Swap the real PayPal client for a fake that returns stubbed API responses, so you can exercise checkout, payment, and webhook flows in your test suite without network calls or sandbox credentials.

View on GitHub
Deep Wiki
Context7

Getting Started

puntodev/paypal-fake is a test double for puntodev/paypal. You install it as a dev dependency and swap the real PayPal client for a fake inside your test suite, so order creation, capture, refunds and webhook flows can be exercised without hitting the sandbox API.

Requirements for 2.0.0 (breaking):

  • PHP 8.5+
  • puntodev/paypal ^7.0

If your app is still on PHP 8.4 or puntodev/paypal 6.x, stay on the 1.x line — 2.0.0 is not a "try it and roll back" upgrade, it hard-requires the new runtime and the new SDK major.

Install:

composer require --dev puntodev/paypal-fake:^2.0

Where to look first:

  1. The package README on GitHub (puntodev/paypal-fake) — the fake API surface is small and the README is the canonical reference for method names and response shapes.
  2. puntodev/paypal's own source — the fake mirrors the real client's public methods, so any method you call in production code must have a corresponding fake hook.
  3. Your existing tests that touch PayPal — these are the ones you convert first.

First use case: a feature test for checkout. Fake the order-creation response, POST to your checkout endpoint, assert the order ID came back and that PayPal was called once with the right amount and currency.

use PuntoDev\PayPal\PayPal;

public function test_checkout_creates_paypal_order(): void
{
    PayPal::fake([
        '*/v2/checkout/orders' => PayPal::response([
            'id' => 'ORDER-123',
            'status' => 'CREATED',
        ], 201),
    ]);

    $this->postJson('/api/checkout', ['sku' => 'ABC', 'qty' => 2])
        ->assertOk()
        ->assertJsonPath('paypal_order_id', 'ORDER-123');

    PayPal::assertOrderCreated(fn ($request) => $request['purchase_units'][0]['amount']['value'] === '20.00');
}

Implementation Patterns

Swap once per test class, not once per test. Register the fake in setUp()/Pest beforeEach(), and let the package's per-test reset clear recorded calls. If you fake() globally in a base TestCase, make sure the reset runs before each test or assertions will leak between tests.

Stub by endpoint pattern, Laravel-HTTP-style. The idiomatic shape is a map of URL patterns to canned responses. Keep patterns narrow (*/v2/checkout/orders/*/capture) rather than a single catch-all, so an unexpected call fails loudly instead of silently returning a 200.

Model the three states you actually care about in production:

// Happy path
PayPal::fake(['*capture' => PayPal::response(['status' => 'COMPLETED'], 201)]);

// Declined / funding failure — PayPal returns 422 with a structured body
PayPal::fake(['*capture' => PayPal::response([
    'name' => 'UNPROCESSABLE_ENTITY',
    'details' => [['issue' => 'INSTRUMENT_DECLINED']],
], 422)]);

// Transport failure — exercises your retry/queue logic
PayPal::fake(['*capture' => PayPal::response('', 500)]);

The 422-with-INSTRUMENT_DECLINED body is the single most valuable fake in this package: it is the branch your checkout code gets least real-world coverage on.

Chain the full order lifecycle in one test. Create → capture → refund, with a sequence of stubbed responses, then assert your local orders table transitioned correctly at each step. This is where the fake pays for itself — no sandbox flakiness, no shared test merchant account, no rate limits in CI.

Fake webhooks at the payload level, not the signature level. Build the webhook body with the package's response helper and call your controller directly, bypassing (or separately unit-testing) signature verification. Do not try to forge real PAYPAL-TRANSMISSION-SIG headers — that is what the real SDK's verification is for, and faking it gives you false confidence.

Assert on intent, not on serialized JSON. Use the assertion helpers with a closure over the request body and check amount, currency, custom_id and idempotency headers. Full-body snapshot assertions break on every benign SDK field addition.

Integration tip — bind in the container for non-HTTP code. If you call the PayPal client from a job, action class, or console command, the fake swap works the same way as long as the client is resolved from the container rather than new'd inline. Constructor-inject PayPal and let the fake take over.

Pair with Http::preventStrayRequests(). If the fake misses a call, you want an exception, not a real request to api-mall.paypal.com from CI.

Gotchas and Tips

The version lock is a two-sided constraint. paypal-fake:^2.0 requires puntodev/paypal:^7.0. Upgrading the fake before the SDK gives you a Composer conflict; upgrading the SDK without checking the fake's major pin gives you a fake that shadows methods that no longer exist. Bump both in the same PR and read the puntodev/paypal 7.0 upgrade notes — anything the real SDK renamed is renamed here too.

PHP 8.5 is the hard blocker. Because this is a require-dev dependency, the constraint only bites in CI and local dev — but it bites hard: composer install on a PHP 8.4 runner fails outright rather than degrading. Pin puntodev/paypal-fake:^1.0 explicitly in projects that aren't ready, and gate the upgrade on your CI matrix, not just your local machine.

Fakes don't cover raw Guzzle. Anything that talks to PayPal outside the puntodev/paypal client — a hand-rolled Guzzle call, an SDK you instantiated yourself, a JS SDK on the frontend — is invisible to the fake. Grep for paypal.com before assuming full coverage.

Response shapes are your responsibility. The fake returns whatever array you hand it. A typo'd key (purchase_unit vs purchase_units) produces a passing test and a production undefined index. Copy real sandbox payloads into fixtures once, store them under tests/Fixtures/paypal/, and reuse.

Watch the access-token layer. The real client fetches and caches an OAuth token before every call; the fake typically short-circuits that. If your code or config depends on token caching behaviour (expiry handling, cache store), the fake will not exercise it — cover that path separately or with a targeted unit test.

Reset between tests. Recorded calls and queued responses persist on the fake instance. If your base TestCase doesn't reset, a test that asserts "exactly one capture" can pass or fail depending on test ordering — the classic flaky-CI symptom.

Partial fakes are a trap. A catch-all fallback that returns 200 OK for unmatched endpoints will hide missing stubs for months. Prefer strict mode, and add stubs as failures surface.

Debugging: when an assertion against a recorded request fails, dump the full recorded request list rather than the single matched call — mismatches are almost always a URL pattern or a nested key, and seeing the actual body settles it in seconds.

Extension points to know: the response helper for status/header control, the request-matching closure for assertions, and the record/reset hooks for custom base-test wiring. If you need behaviour the fake doesn't expose, prefer a thin wrapper trait in your own test suite over forking the package — the fake's API tracks the SDK major, so local forks rot fast.

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.
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen