Back to Course |
Testing in Laravel 11: Advanced Level

Asserting JSON Structure

In the Testing For Beginners course, we tested that the response is JSON. However, you might need to test the JSON structure more deeply when creating APIs. Let's see how we can do it.


Laravel Code

In this example, we have a route in routes/api.php and a CRUD Controller. However, the products list is returned using the API Resource, and not all fields are returned deliberately for this example.

routes/api.php:

use App\Http\Controllers\Api\ProductController;
 
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
 
Route::apiResource('products', ProductController::class);

app/Http/Controllers/Api/ProductController.php:

use App\Models\Product;
use App\Http\Controllers\Controller;
use App\Http\Resources\ProductResource;
use Illuminate\Http\Resources\Json\JsonResource;
 
class ProductController extends Controller
{
public function index(): JsonResource
{
return ProductResource::collection(Product::all());
}
 
public function show(Product $product): JsonResource
{
return ProductResource::make($product);
}
 
public function update(Request $request, Product $product): JsonResource
{
$product->update($request->all());
 
return ProductResource::make($product);
}
}

app/Http/Resources/ProductResource.php:

class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
];
}
}

When we test the /api/products endpoint using the API client, we will get a list of products from the DB.


JSON Data Anywhere & JSON Count

Using the assertJsonFragment(), you can test whether the response contains given data anywhere.

The API Resource returns a partial model, so we can check only a few values if they exist somewhere in the response. Also, using assertJsonFragment() does not consider that the response is wrapped within the data key.

Another useful assertion is assertJsonCount(). When you get a response and know how many records should be inside, assertJsonCount() can help you test that number.

use App\Models\Product;
use function Pest\Laravel\getJson;
 
test('api returns products list', function () {
$product1 = `keyProduct::factory()->create();
$product2 = Product::factory()->create();
 
getJson('/api/products')
->assertJsonFragment([
'name' => $product1->name,
'price' => $product1->price,
])
->assertJsonCount(2, 'data');
});

Fluent JSON Testing

Laravel provides a way to test JSON responses fluently. To test this way, you return a closure inside the assertJson() assertion. This closure will accept an instance of Illuminate\Testing\Fluent\AssertableJson, which will make assertions inside the closure you use where() with the exact location of the key to check if the key and value exist and missing() to see if the key doesn't exist.

Another useful method is etc(). This method informs Laravel that there might be more JSON objects.

use App\Models\Product;
use Illuminate\Testing\Fluent\AssertableJson;
use function Pest\Laravel\getJson;
 
test('fluent products list test', function () {
$product1 = Product::factory()->create();
$product2 = Product::factory()->create();
 
getJson('/api/products')
->assertJson(fn (AssertableJson $json) =>
$json->where('data.0.name', $product1->name)
->where('data.0.price', $product1->price)
->missing('data.0.created_at')
->etc()
);
});

JSON Path & Structure

You might want to test whether a path from the API exists or has the exact structure. For such a task, there are three assertions:

  • assertJsonPath()
  • assertJsonMissingPath()
  • assertJsonStructure()

We can show that in the endpoint of showing the product.

use App\Models\Product;
use function Pest\Laravel\getJson;
 
test('api shows products', function () {
$product = Product::factory()->create();
 
getJson('/api/products/' . $product->id)
->assertJsonPath('data.name', $product->name)
->assertJsonMissingPath('data.created_at')
->assertJsonStructure([
'data' => [
'id',
'name',
'price',
]
]);
});

Or, if you prefer expectations from Pest, you can achieve the same with this syntax:

use App\Models\Product;
use function Pest\Laravel\getJson;
 
test('api shows products', function () {
$product = Product::factory()->create();
 
$resp = getJson('/api/products/' . $product->id);;
 
expect($resp->json('data'))
->toHaveKey('name')
->not->toHaveKey('created_at')
->toHaveKeys(['id', 'name', 'price'])
->name->toEqual($product->name);
});

JSON Missing

A typical example could be when a record is updated to check if the original value is not returned.

test('api updates product', function () {
$product = Product::factory()->create();
 
putJson('/api/products/' . $product->id, [
'name' => 'new name',
'price' => 100,
])
->assertJsonMissing(['name' => $product->name, 'price' => $product->price]);
});

Or, again, if you prefer the expectations from Pest:

use App\Models\Product;
use function Pest\Laravel\putJson;
 
test('api updates product', function () {
$product = Product::factory()->create();
 
$resp = putJson('/api/products/' . $product->id, [
'name' => 'new name',
'price' => 100,
]);
 
expect($resp->json('data'))
->name->not->toBe($product->name)
->price->not->toBe($product->price);
});

Of course, there are more JSON assertions, which you can find in the Laravel documentation.