Laravel Apiable ships with testing utilities built on top of PHPUnit and Laravel's test response helpers. They give you a fluent, chainable API for asserting the shape and content of your JSON:API responses.
No extra configuration is required. The assertJsonApi macro is registered on Illuminate\Testing\TestResponse automatically when the package service provider boots.
The entry point for all JSON:API assertions. Call it on any TestResponse instance.
Without a callback it validates that the response is a structurally valid JSON:API document (must contain at least one of data, errors, or meta) and returns the response so you can continue chaining standard Laravel assertions.
$response = $this->getJson('/posts');
$response->assertJsonApi();
Pass a closure to receive an AssertableJsonApi instance and make further assertions:
use OpenSoutheners\LaravelApiable\Testing\AssertableJsonApi;
$response = $this->getJson('/posts/1');
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasType('post')->hasId(1);
});
The AssertableJsonApi class extends Laravel's AssertableJson, so every parent method — where, has, missing, count, first, each, etc, dd, dump, when, tap — is fully available inside any scope.
Assert that the response contains a single resource (not a collection).
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->isResource();
});
Assert that the response contains a collection (list of resources).
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->isCollection();
});
Scope into the resource at the given zero-based position in the collection. Returns a new AssertableJsonApi scoped to that item, so you can chain further assertions against it.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->at(0)->hasAttribute('title', 'Hello world');
$assert->at(1)->hasType('post');
});
Pass an optional closure as a second argument to run assertions inside that scope and have the method return the collection instance for continued chaining:
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->at(0, function (AssertableJsonApi $item) {
$item->hasAttribute('title', 'Hello world')->etc();
})->hasSize(3);
});
Note: When using the closure form, call
etc()at the end of the closure if you do not want PHPUnit to fail for properties you have not explicitly asserted. This is the standardAssertableJsonbehaviour.
Assert the number of resources present in the collection.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasSize(5);
});
Assert that the current resource has the given ID. The value is cast to string internally, so passing an integer or string both work.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasId(1);
});
Assert that the current resource has the given JSON:API type string.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasType('post');
});
Assert that the resource has the specified attribute key. Pass a second argument to also assert the exact value using a strict (===) comparison.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasAttribute('title');
$assert->hasAttribute('title', 'Hello world');
});
Breaking change from v4.2: The value comparison is now strict (
assertSame). Previously the check usedassertContainson the attributes array, which could pass even when the value matched a different attribute key.
Assert that the resource does not have the specified attribute key. When a second argument is provided, the assertion passes if the key is absent or the key exists with a different value.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasNotAttribute('secret');
$assert->hasNotAttribute('title', 'Forbidden title');
});
Assert multiple attributes at once. Accepts a map of name → value pairs (asserts key existence and exact value) or a list of names (asserts key existence only).
$response->assertJsonApi(function (AssertableJsonApi $assert) {
// map form: assert key + exact value
$assert->hasAttributes([
'title' => 'Hello world',
'slug' => 'hello-world',
]);
// list form: assert key existence only
$assert->hasAttributes(['title', 'slug', 'body']);
});
Assert that multiple attributes are absent (or do not hold the given values).
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->hasNotAttributes([
'title' => 'Forbidden title',
'slug' => 'forbidden-slug',
]);
});
These methods scope the fluent assertion context into a specific JSON:API document member. Inside the callback you have the full AssertableJson API — where, has, missing, count, etc, and so on. Call etc() at the end of a callback whenever you only check a subset of the properties in that scope.
Scope into the top-level data member.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->data(function (AssertableJsonApi $data) {
$data->where('type', 'post')
->where('id', '1')
->has('attributes')
->etc();
});
});
Scope into the top-level meta member.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->meta(function (AssertableJsonApi $meta) {
$meta->where('current_page', 1)
->where('total', 42)
->etc();
});
});
Scope into the top-level links member.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->links(function (AssertableJsonApi $links) {
$links->has('next')->etc();
});
});
Scope into the top-level errors member. Useful for asserting validation or business error responses. An errors document must not contain a data member — assertJsonApi enforces this at parse time.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->errors(function (AssertableJsonApi $errors) {
$errors->count(2)
->where('0.status', '422')
->where('0.title', 'Invalid input')
->etc();
});
});
Scope into the top-level included array.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->included(function (AssertableJsonApi $included) {
$included->count(2)->etc();
});
});
Scope into the data of a named relationship on the current resource (i.e. relationships.{name}.data).
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->relationship('author', function (AssertableJsonApi $author) {
$author->where('type', 'client')->has('id');
});
});
Navigate to an included resource by its model instance and return a new AssertableJsonApi scoped to it. The model must be present in the response's included array.
$relatedComment = Comment::find(4);
$response->assertJsonApi(function (AssertableJsonApi $assert) use ($relatedComment) {
$assert->at(0)
->atRelation($relatedComment)
->hasAttribute('content', 'Foo bar');
});
Pass an optional closure to run assertions inside the scope and return $this instead:
$assert->at(0)->atRelation($relatedComment, function (AssertableJsonApi $rel) {
$rel->hasAttribute('content', 'Foo bar')->etc();
});
Assert that the resource has at least one relationship of the given resource type. Pass a model class string or instance — the type is resolved automatically.
Set the second argument to true to also assert that the related resources appear in the included top-level key.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->at(0)->hasAnyRelationships('comment', true);
});
Assert that the resource has no relationships of the given resource type. Set the second argument to true to also assert that no resources of that type appear in included.
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->at(0)->hasNotAnyRelationships('comment', true);
});
Assert that a specific model instance is linked as a relationship of the current resource. Set the second argument to true to also verify the model appears in included.
$relatedComment = Comment::find(4);
$response->assertJsonApi(function (AssertableJsonApi $assert) use ($relatedComment) {
$assert->hasRelationshipWith($relatedComment, true);
});
Assert that a specific model instance is not linked as a relationship of the current resource. Set the second argument to true to also verify the model is absent from included.
$unrelatedComment = Comment::find(99);
$response->assertJsonApi(function (AssertableJsonApi $assert) use ($unrelatedComment) {
$assert->hasNotRelationshipWith($unrelatedComment, true);
});
Because AssertableJsonApi fully extends AssertableJson, you can use any of its methods inside the document scoping callbacks:
| Method | Description |
|---|---|
where($key, $value) |
Assert an exact value at a dot-path |
whereNot($key, $value) |
Assert a value is not equal |
whereContains($key, $value) |
Assert a value is contained |
has($key) |
Assert a key exists |
missing($key) |
Assert a key does not exist |
count($key, $n) |
Assert the array at key has n items |
etc() |
Allow unchecked properties in the current scope |
dd() / dump() |
Dump the current scope for debugging |
when($condition, $callback) |
Conditional assertions |
tap($callback) |
Tap into the chain without changing it |
$response->assertJsonApi(function (AssertableJsonApi $assert) {
$assert->data(function (AssertableJsonApi $data) {
$data->where('type', 'post')
->has('attributes')
->missing('password')
->etc();
});
$assert->meta(fn ($meta) => $meta->where('total', 10)->etc());
});
hasAttribute value comparisonThe old implementation used assertContains($value, $attributes), which matched the value against any attribute regardless of key. The new implementation uses assertSame($value, $attributes[$name]).
// Before (could pass even if 'Hello' was under a different key)
$assert->hasAttribute('subtitle', 'Hello');
// After (checks the exact key)
$assert->hasAttribute('subtitle', 'Hello'); // fails if attributes['subtitle'] !== 'Hello'
Before v4.3, fromTestResponse silently scoped a collection response to the first item, so hasAttribute, hasId, hasType, and relationship methods worked directly on the collection root. This behaviour is removed.
// Before (implicitly operated on the first item)
$assert->hasAttribute('title', 'Hello');
$assert->hasAnyRelationships('comment', true);
// After — use at() to scope into a specific item first
$assert->at(0)->hasAttribute('title', 'Hello');
$assert->at(0)->hasAnyRelationships('comment', true);
toArray() now returns the full document props (delegating to the parent AssertableJson::toArray()) instead of only the current resource's attributes.
// Before
$assert->toArray(); // ['title' => '...', 'slug' => '...']
// After
$assert->toArray(); // ['data' => ['id' => '...', 'type' => '...', 'attributes' => [...]], ...]
All assertion methods return $this (or a new AssertableJsonApi instance for navigation methods), so you can chain them freely:
use OpenSoutheners\LaravelApiable\Testing\AssertableJsonApi;
$relatedTag = Tag::find(1);
$response->assertJsonApi(function (AssertableJsonApi $assert) use ($relatedTag) {
$assert->isCollection()
->hasSize(3)
->at(0)
->hasType('post')
->hasAttribute('title', 'Hello world')
->hasRelationshipWith($relatedTag, true);
});
How can I help you explore Laravel packages today?