tourze/phpunit-json-rpc
A PHPUnit extension for testing JSON-RPC services. It helps PHP developers send requests, validate responses, and assert protocol behavior in automated test suites, making JSON-RPC API integration testing simpler and more reliable.
tourze/phpunit-json-rpc ships as a dev-only dependency that gives PHPUnit assertions for JSON-RPC 2.0 envelopes, so you spend less time hand-writing assertArrayHasKey('result', ...) boilerplate.
composer require --dev tourze/phpunit-json-rpc:^1.0
1.0.0 is the first tagged stable release — the changelog link contains no migration notes, which means there is no previous API to break from, but it also means the surface is freshly frozen. Treat ^1.0 as your constraint and read src/ before assuming method names; the package is small enough to audit in a few minutes.
Where to look first, in order:
README.md — usually the only prose documentation for Tourze packages.src/ — the assertion trait / base test case and its public method signatures. This is the real API reference.tests/ — the package's own tests double as usage examples you can copy verbatim.First use case — assert a successful call end-to-end against a Laravel route:
use Tourze\PHPUnitJsonRpc\JsonRpcAssertions; // verify exact namespace in src/
class RpcTest extends TestCase
{
use JsonRpcAssertions;
public function test_ping(): void
{
$response = $this->postJson('/rpc', [
'jsonrpc' => '2.0',
'method' => 'system.ping',
'params' => [],
'id' => 1,
]);
$response->assertOk();
$this->assertJsonRpcResult($response->json(), expected: 'pong');
}
}
If the trait's helpers don't match your naming instincts, wrap them in a project-local BaseRpcTestCase on day one — see below.
Envelope assertion, not payload assertion. The core value of a JSON-RPC assertion library is enforcing the shape: jsonrpc === "2.0", presence of exactly one of result / error, and id echo. Structure your tests so the library checks the envelope and a plain assertSame checks your domain value. Mixing the two makes failures ambiguous.
Base test case as the integration seam. Rather than use-ing the trait in every test class, create one abstract RpcTestCase in tests/ that applies the trait, defines a rpc(string $method, array $params = [], int|string|null $id = 1) helper posting to your endpoint, and exposes your auth/tenant setup. Every RPC test then extends that. This is where you absorb any API churn from a 1.x minor release in one place.
Data providers for method matrices. JSON-RPC endpoints are naturally table-driven — same transport, many methods. Put [method, params, expectedResult] tuples in a provider and assert the envelope once per case. This keeps coverage broad without duplicating setup.
Error-path pattern. For each documented error code, assert both the code and the absence of result. A response containing both result and error is spec-invalid but will pass a naive "has error key" check — assert mutual exclusivity explicitly, ideally via the package's error assertion plus a assertArrayNotHasKey('result', ...).
Batch requests. For [{...}, {...}], remember the spec does not guarantee response ordering; the spec only requires each response carry the id of its request. Sort by id (or map by id) before asserting. If the package offers a batch helper, confirm whether it sorts for you — read the source rather than trusting the name.
Notification coverage. Methods invoked without id must produce an empty body (HTTP 204 or 200 with no content). These are easy to forget and are a common source of production-only bugs; add one test per notification-capable method.
Contract testing against a schema. If you already maintain an OpenAPI/JSON Schema for the RPC surface, layer the package's envelope assertions underneath a schema validation of result. The two are complementary: schema catches payload drift, this package catches protocol drift.
id type fidelity. The spec requires the response id to be the same JSON type as the request. "1" and 1 are different. Use assertSame, never assertEquals, or a coercion bug in your handler will slip through.result: null vs absent result. A valid response may legitimately have result: null for a void method. assertArrayHasKey is the correct check; isset() is not.json_encode([]) yields [], but JSON-RPC params/results often require {}. If your assertions decode to arrays, this distinction collapses and you cannot test it. Assert on the raw body string where it matters.assertOk() and then the error envelope. A 422 usually means Laravel's validation layer rejected the request before your RPC handler saw it — a different bug class worth its own test.phpunit/phpunit in require-dev and occasionally in require. Check composer.json before installing into a Laravel 11/12 app on PHPUnit 10/11 — a hard ^9 constraint will force a resolution conflict or a downgrade. If it conflicts, composer require --dev -W and inspect what it wants to move.Tests\TestCase and avoids fighting Laravel's bootstrap. The base class is only worth it if the package does its own setUp work you need.$response->getContent() (raw, not ->json()) — decoded arrays hide key-ordering, duplicate keys, and numeric precision issues that the raw string reveals.result. assertEquals on floats is fragile. Compare with an epsilon, or compare the string form. This bites most often on financial/timestamp results.assertRpcError(int $code, ?string $message = null) that delegates to the package and adds your project's conventions (e.g. asserting data.trace_id is present). Keep it in the base test case.composer.lock, and re-read src/ on any 1.x bump before letting CI pull it. Watch the repo for a 1.0.1 — first stable releases of small packages commonly collect immediate patch fixes.How can I help you explore Laravel packages today?