rasuvaeff / payments-testing
Deterministic fake payment gateways and framework-agnostic contract assertions for testing
Requires
- php: 8.3 - 8.5
- rasuvaeff/payments: ^0.1
Requires (Dev)
- ergebnis/composer-normalize: ^2.51
- friendsofphp/php-cs-fixer: ^3.95
- infection/infection: ^0.33
- maglnet/composer-require-checker: ^4.17
- rasuvaeff/rector-named-literals: ^1.0
- rector/rector: ^2.4
- roave/backward-compatibility-check: ^8.0
- testo/bridge-infection: ^0.1.6
- testo/testo: ^0.10.25
- vimeo/psalm: ^6.16
This package is auto-updated.
Last update: 2026-08-19 06:18:09 UTC
README
Testing toolkit for the rasuvaeff/payments contracts: deterministic in-memory
fake gateways for unit tests and local development, plus framework-agnostic
contract assertions that verify any real gateway adapter honours the same
contracts the fakes do.
Using an AI coding assistant? llms.txt contains a compact API reference.
Requirements
- PHP 8.3+
rasuvaeff/payments
Installation
composer require --dev rasuvaeff/payments-testing
Usage
Fake gateways
use Rasuvaeff\PaymentsTesting\FakePaymentGateway; $gateway = new FakePaymentGateway(); $attempt = $gateway->createPayment($request); $sameAttempt = $gateway->retrievePayment(new RetrievePaymentRequest( operationId: new OperationId(value: 'reconcile-1'), payment: $attempt->payment, ));
The fakes are fully deterministic:
- Calls with the same idempotency key and the same request fingerprint return
the same provider reference. The key is the whole scope: a provider never
learns which
OperationIdissued the call, so two operations sharing one key replay each other exactly as the real gateway would. - Reusing a key with different request data throws; calls without a key receive distinct deterministic references.
- References from another provider and unknown references are rejected.
- Time is fixed through
FakeGatewayConfig(now, default2025-01-01T00:00:00+00:00) — no wall clock involved.
Each fake implements only the optional operation its class names; the base fake never implements capture, confirm, cancel or refund implicitly.
| Type | Optional interface |
|---|---|
FakePaymentGateway |
None; base gateway only |
FakeCaptureGateway |
CaptureGatewayInterface |
FakeConfirmGateway |
ConfirmGatewayInterface |
FakeCancelGateway |
CancelGatewayInterface |
FakeRefundGateway |
RefundGatewayInterface |
FakeGatewayConfig |
Provider, capabilities, fixed time and create state |
Configuration:
use Rasuvaeff\PaymentsTesting\FakeGatewayConfig; use Rasuvaeff\PaymentsTesting\FakeRefundGateway; $gateway = new FakeRefundGateway(config: new FakeGatewayConfig( provider: new PaymentProvider(value: 'fake'), capabilities: CapabilitySet::of(new PartialRefundCapability()), now: new \DateTimeImmutable('2025-06-01T12:00:00+00:00'), createState: PaymentState::Pending, ));
PartialRefundCapability in the capability set of a non-refund fake throws
InvalidArgumentException — capabilities must stay truthful.
Contract assertions
Static assertion classes run a real (or fake) gateway adapter through the
contract and throw ContractViolationException on any violation. They are
framework-agnostic — an uncaught exception fails the test in any framework
(Testo, PHPUnit, Pest, plain PHP script).
use Rasuvaeff\PaymentsTesting\PaymentGatewayAssertions; $capabilities = PaymentGatewayAssertions::assertCapabilities($gateway); $attempt = PaymentGatewayAssertions::assertCreatePayment($gateway, $createRequest); // Issues four calls: the key twice, then a distinct key, then the key with a // changed amount. Checking only the replay would pass a gateway that ignores // the key entirely and always answers with the same reference. PaymentGatewayAssertions::assertCreatePaymentIdempotency($gateway, $keyedRequest); PaymentGatewayAssertions::assertRetrievePayment($gateway, new RetrievePaymentRequest( operationId: new OperationId(value: 'reconcile-1'), payment: $attempt->payment, ));
| Class | Method | Verifies |
|---|---|---|
PaymentGatewayAssertions |
assertCapabilities() |
PartialRefundCapability implies RefundGatewayInterface |
PaymentGatewayAssertions |
assertCreatePayment() |
Provider consistency, operation id and amount preserved |
PaymentGatewayAssertions |
assertCreatePaymentIdempotency() |
Replaying a key returns the same payment, a different key starts a new one, and reusing a key with a changed request is refused |
PaymentGatewayAssertions |
assertRetrievePayment() |
Provider consistency, operation id and payment reference preserved |
CaptureGatewayAssertions |
assertCapturePayment() |
Capture preserves operation id and amount, provider consistent |
ConfirmGatewayAssertions |
assertConfirmPayment() |
Confirm preserves operation id and payment reference |
CancelGatewayAssertions |
assertCancelPayment() |
Cancel preserves operation id and payment reference |
RefundGatewayAssertions |
assertCreateRefund() |
Refund provider/payment/refund consistency, requested amount preserved |
RefundGatewayAssertions |
assertCreateRefundIdempotency() |
Replaying a key returns the same refund, a different key starts a new one, and reusing a key with a changed amount is refused |
RefundGatewayAssertions |
assertRetrieveRefund() |
Provider consistency, operation id and refund reference preserved |
Capability-specific classes require the intersection type
(e.g. PaymentGatewayInterface&CaptureGatewayInterface), so calling them on an
adapter that does not implement the optional interface is a type error, not a
runtime surprise. Every assertion method returns the resulting attempt for
further framework-specific checks.
Idempotency assertions require idempotencyKey on the request and throw
InvalidArgumentException when it is missing.
Security
The package is test-only. Never place production credentials, PAN/CVC or raw webhook bodies in fake requests. Idempotency fingerprints are kept in memory and contain only normalized request values.
Examples
See examples/.
| Script | Shows | Needs server? |
|---|---|---|
create-payment.php |
Deterministic create and retrieve flow | No |
Development
docker run --rm -v "$PWD":/app -w /app composer:2 composer build
License
BSD-3-Clause