# Resource Tests

# Introduction

Laravel JSON:API provides a very fluent API for making JSON:API HTTP requests to your application, and examining the output.

This chapter provides examples of testing resource actions: i.e. the index, store, show, update and destroy actions. In these examples, our application is a blog with a posts resource.

# Index Testing

The index action returns zero-to-many resources. For our posts resource, we can test this as follows:

use App\Models\Post;

public function test(): void
{
    $posts = Post::factory()->count(3)->create();

    $response = $this
        ->jsonApi()
        ->expects('posts')
        ->get('/api/v1/posts');

    $response->assertFetchedMany($posts);
}

In this test, we create a JSON:API request to the GET /api/v1/posts route. The expects method tells the test that the response should contain posts resources. This enables us to pass models to the assertions, as the test will know that the models should be serialized as posts resources.

Our assertion - assertFetchedMany - checks that the response contains a JSON:API document that contains a resource collection (i.e. zero-to-many). It uses the provided models to check that they are the only resources in the response. I.e. in this test, it checks there are 3 posts resources with ids that match the provided models.

TIP

If you provide models to our test assertion methods, the getRouteKey method is used to work out the resource's id.

# Store (aka Create) Testing

We can test creating a posts resource by using a model factory to make a Post model. We use this to build an array of JSON:API data for the test request:

use App\Models\Post;
use App\Models\Tag;

public function test(): void
{
    $post = Post::factory()->make();
    $tags = Tag::factory()->count(2)->create();

    $data = [
        'type' => 'posts',
        'attributes' => [
            'content' => $post->content,
            'slug' => $post->slug,
            'title' => $post->title,
        ],
        'relationships' => [
            'tags' => [
                'data' => $tags->map(fn(Tag $tag) => [
                    'type' => 'tags',
                    'id' => (string) $tag->getRouteKey(),
                ])->all(),
            ],
        ],
    ];

    $response = $this
        ->actingAs($post->author)
        ->jsonApi()
        ->expects('posts')
        ->withData($data)
        ->includePaths('tags')
        ->post('/api/v1/posts');

    $id = $response
        ->assertCreatedWithServerId('http://localhost/api/v1/posts', $data)
        ->id();

    $this->assertDatabaseHas('posts', [
        'id' => $id,
        'author_id' => $post->author_id,
        'content' => $post->content,
        'slug' => $post->slug,
        'title' => $post->title,
    ]);

    foreach ($tags as $tag) {
        $this->assertDatabaseHas('taggables', [
            'tag_id' => $tag->getKey(),
            'taggable_id' => $id,
            'taggable_type' => Post::class,
        ]);
    }
}

In this test, we expect the authenticated user to be stored as the posts resource's author. We therefore use Laravel's actingAs helper to authenticate before calling the jsonApi method.

The JSON:API specification states that the server must respond to a create request with a 201 Created status and a Location header. The assertCreatedWithServerId method checks the response for this status and header, and then checks that the response JSON:API document has the expected $data in it.

Finally, we use the id method on our response to read the resource's id from the response. As shown, we can use this when making database assertions to check the data has been correctly stored.

# Show (aka Read) Testing

We can test reading a specific posts resource as follows:

use App\Models\Post;

public function test(): void
{
    $post = Post::factory()->create();
    $self = 'http://localhost/api/v1/posts/' . $post->getRouteKey();

    $expected = [
        'type' => 'posts',
        'id' => (string) $post->getRouteKey(),
        'attributes' => [
            'content' => $post->content,
            'createdAt' => $post->created_at->jsonSerialize(),
            'slug' => $post->slug,
            'title' => $post->title,
            'updatedAt' => $post->updated_at->jsonSerialize(),
        ],
        'relationships' => [
            'author' => [
                'links' => [
                    'self' => "{$self}/relationships/author",
                    'related' => "{$self}/author",
                ],
            ],
            'comments' => [
                'links' => [
                    'self' => "{$self}/relationships/comments",
                    'related' => "{$self}/comments",
                ],
            ],
            'tags' => [
                'links' => [
                    'self' => "{$self}/relationships/tags",
                    'related' => "{$self}/tags",
                ],
            ],
        ],
        'links' => [
            'self' => $self,
        ],
    ];

    $response = $this
        ->jsonApi()
        ->expects('posts')
        ->get($self);

    $response->assertFetchedOneExact($expected);
}

In this test, we are going to check the entire serialized posts resource. Therefore our $expected array contains the complete expected resource. We use assertFetchedOneExact to do an exact match with the /data member of the JSON:API response.

TIP

If our $expected array only contained a partial serialization of the posts resource, we would use assertFetchedOne instead.

# Update Testing

We can test updating a posts resource as follows:

use App\Models\Post;
use App\Models\Tags;

public function test(): void
{
    $post = Post::factory()->create();
    $post->tags()->attach($existing = Tag::factory()->create());
    $tags = Tag::factory()->count(2)->create();

    $data = [
        'type' => 'posts',
        'id' => (string) $post->getRouteKey(),
        'attributes' => [
            'content' => 'This is new article content.',
            'slug' => 'article-is-updated',
            'title' => 'Updated Article',
        ],
        'relationships' => [
            'tags' => [
                'data' => $tags->map(fn(Tag $tag) => [
                    'type' => 'tags',
                    'id' => (string) $tag->getRouteKey(),
                ])->all(),
            ],
        ],
    ];

    $response = $this
        ->actingAs($post->author)
        ->jsonApi()
        ->expects('posts')
        ->includePaths('tags')
        ->withData($data)
        ->patch('/api/v1/posts' . $post->getRouteKey());

    $response->assertFetchedOne($expected);

    $this->assertDatabaseHas('posts', [
        'id' => $post->getKey(),
        'author_id' => $post->author_id,
        'content' => $data['attributes']['content'],
        'slug' => $data['attributes']['slug'],
        'title' => $data['attributes']['title'],
    ]);

    /** The existing tag should have been detached. */
    $this->assertDatabaseMissing('taggables', [
        'tag_id' => $existing->getKey(),
        'taggable_id' => $post->getKey(),
        'taggable_type' => Post::class,
    ]);

    /** These tags should have been attached. */
    foreach ($tags as $tag) {
        $this->assertDatabaseHas('taggables', [
            'tag_id' => $tag->getKey(),
            'taggable_id' => $post->getKey(),
            'taggable_type' => Post::class,
        ]);
    }
}

In this test, we create an existing post and a JSON:API document that will change both the posts resource and its associated tags. The assertFetchedOne method checks that the response was a success and contains a JSON:API document that matches the provided data. We then use database assertions to ensure that the updated data has been stored in the database.

# Destroy (aka Delete) Testing

We can test deleting a posts resource as follows:

use App\Models\Post;

public function test(): void
{
    $post = Post::factory()->create();

    $response = $this
        ->actingAs($post->author)
        ->jsonApi()
        ->delete('/api/v1/posts' . $post->getRouteKey());

    $response->assertNoContent();

    $this->assertDatabaseMissing('posts', [
        'id' => $post->getKey(),
    ]);
}

In this example, we assert that we have received a 204 No Content response. We then check that the Post model no longer exists in the database.

TIP

The JSON:API specification says that:

  • A server MUST return a 204 No Content status code if a deletion request is successful and no content is returned.
  • A server MUST return a 200 OK status code if a deletion request is successful and the server responds with only top-level meta data.

Therefore you should use either assertNoContent or assertMetaWithoutData in your delete test.

Last Updated: 12/1/2020, 1:43:19 PM