Relationship Tests
Introduction
This chapter provides examples of testing relationship routes: i.e. the related
, show
, update
, attach
and detach
relationship actions.
The examples involve both to-one relations, and to-many relations.
To-One
To demonstrate tests for a to-one relation, we will imagine that our posts
resource has an image
relation.
Related Resource Testing
To test that we can read the related images
resource, we will use the following test:
use App\Models\Image;
use App\Models\Post;
public function test(): void
{
$post = Post::factory()->for(Image::factory())->create();
$image = $post->image;
$expected = [
'type' => 'images',
'id' => (string) $image->getRouteKey(),
'attributes' => [
'url' => $image->url,
],
];
$response = $this
->jsonApi()
->expects('images')
->get("/api/v1/posts/{$post->getRouteKey()}/image");
$response->assertFetchedOne($expected);
}
We are expecting the GET /api/v1/posts/{post}/image
route to return the related images
resource. We therefore ensure our $expected
data includes at least one attribute or relationship. We use assertFetchedOne
to ensure the response is a 200 OK
response with our expected data in the data
member of the JSON:API document.
TIP
If we wanted to test an empty relationship, we would use the assertFetchedNull
assertion.
Show To-One Testing
To test that we can read the related images
resource identifier, we will use the following test:
use App\Models\Image;
use App\Models\Post;
public function test(): void
{
$post = Post::factory()->for(Image::factory())->create();
$image = $post->image;
$response = $this
->jsonApi()
->expects('images')
->get("/api/v1/posts/{$post->getRouteKey()}/relationships/image");
$response->assertFetchedToOne($image);
}
We are expecting the GET /api/v1/posts/{post}/relationships/image
route to return the resource identifier of the related images
resource. The assertFetchedToOne
assertion does this: it will check that a resource identifier for the provided Image
model in the /data
member of the response.
TIP
If we wanted to test an empty relationship, we would use the assertFetchedNull
assertion.
Update To-One Testing
We can also test updating the relationship as follows:
use App\Models\Image;
use App\Models\Post;
public function test(): void
{
$post = Post::factory()->create();
$image = Image::factory()->create();
$data = [
'type' => 'images',
'id' => (string) $image->getRouteKey(),
];
$response = $this
->jsonApi()
->expects('images')
->withData($data)
->patch("/api/v1/posts/{$post->getRouteKey()}/relationships/image");
$response->assertFetchedToOne($image);
$this->assertDatabaseHas('posts', [
'id' => $post->getKey(),
'image_id' => $image->getKey(),
]);
}
In this request, we provide a resource identifier for the Image
model as our request data. The expected outcome is that the Image
is attached to the Post
model. We use a database assertion to check this has happened.
To-Many
To demonstrate tests for a to-many relation, we will imagine that our posts
resource has a tags
relation.
Related Resources Testing
To test that we can read the related tags
resources, we will use the following test:
use App\Models\Post;
use App\Models\Tag;
public function test(): void
{
$post = Post::factory()->create();
$tags = Tag::factory()->count(2)->create();
$post->tags()->attach($tags);
$expected = $tags->map(fn(Tag $tag) => [
'type' => 'tags',
'id' => (string) $tag->getRouteKey(),
'attributes' => [
'name' => $tag->name,
],
])->all();
$response = $this
->jsonApi()
->expects('tags')
->get("/api/v1/posts/{$post->getRouteKey()}/tags");
$response->assertFetchedMany($expected);
}
We are expecting the GET /api/v1/posts/{post}/tags
route to return the related tags
resources. We therefore ensure our $expected
data includes at least one attribute or relationship. We use assertFetchedMany
to ensure the response is a 200 OK
response with our expected data in the data
member of the JSON:API document.
TIP
If we wanted to test an empty relationship, we would use the assertFetchedNone
assertion.
Show To-Many Testing
To test that we can read the related tags
resource identifiers, we will use the following test:
use App\Models\Post;
use App\Models\Tag;
public function test(): void
{
$post = Post::factory()->create();
$tags = Tag::factory()->count(2)->create();
$post->tags()->attach($tags);
$response = $this
->jsonApi()
->expects('tags')
->get("/api/v1/posts/{$post->getRouteKey()}/relationships/tags");
$response->assertFetchedToMany($tags);
}
We are expecting the GET /api/v1/posts/{post}/relationships/tags
route to return the resource identifiers of the related tags
resources. The assertFetchedToMany
assertion does this: it will check that resource identifiers for the provided Tag
models are in the /data
member of the response.
TIP
If we wanted to test an empty relationship, we would use the assertFetchedNone
assertion.
Update To-Many Testing
The follow example tests updating a to-many relation:
use App\Models\Post;
use App\Models\Tag;
public function test(): void
{
$post = Post::factory()->create();
$post->tags()->attach($existing = Tag::factory()->create());
$tags = Tag::factory()->count(2)->create();
$data = $tags->map(fn(Tag $tag) => [
'type' => 'tags',
'id' => (string) $tag->getRouteKey(),
])->all();
$response = $this
->jsonApi()
->expects('tags')
->withData($data)
->patch("/api/v1/posts/{$post->getRouteKey()}/relationships/tags");
$response->assertFetchedToMany($tags);
/** 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 request, we provide resource identifiers for the Tag
models as our request data. The expected outcome is that the existing Tag
model is detached, and the new Tag
models are attached. This is because a PATCH
request for a to-many relationship must replace the contents of the relationship. I.e. it is a sync operation.
Our database assertions check the database has been updated correctly.
Attach To-Many Testing
The follow example tests attaching new records to a to-many relation:
use App\Models\Post;
use App\Models\Tag;
public function test(): void
{
$post = Post::factory()->create();
$post->tags()->attach($existing = Tag::factory()->create());
$tags = Tag::factory()->count(2)->create();
$data = $tags->map(fn(Tag $tag) => [
'type' => 'tags',
'id' => (string) $tag->getRouteKey(),
])->all();
$response = $this
->jsonApi()
->expects('tags')
->withData($data)
->post("/api/v1/posts/{$post->getRouteKey()}/relationships/tags");
$response->assertNoContent();
/** The existing tag should still be attached. */
$this->assertDatabaseHas('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 request, we provide resource identifiers for the new Tag
models as our request data. The expected outcome of the POST
method is that the new tags are attached, and the existing tag remains attached. Our database assertions check the database has been updated correctly.
Detach To-Many Testing
The follow example tests detaching records to a to-many relation:
use App\Models\Post;
use App\Models\Tag;
public function test(): void
{
$post = Post::factory()->create();
$post->tags()->attach($keep = Tag::factory()->create());
$post->tags()->attach($detach = Tag::factory()->count(2)->create());
$data = $detach->map(fn(Tag $tag) => [
'type' => 'tags',
'id' => (string) $tag->getRouteKey(),
])->all();
$response = $this
->jsonApi()
->expects('tags')
->withData($data)
->delete("/api/v1/posts/{$post->getRouteKey()}/relationships/tags");
$response->assertNoContent();
/** The existing tag should still be attached. */
$this->assertDatabaseHas('taggables', [
'tag_id' => $keep->getKey(),
'taggable_id' => $post->getKey(),
'taggable_type' => Post::class,
]);
/** These tags should have been detached. */
foreach ($detach as $tag) {
$this->assertDatabaseMissing('taggables', [
'tag_id' => $tag->getKey(),
'taggable_id' => $post->getKey(),
'taggable_type' => Post::class,
]);
}
}
In this request, we provide resource identifiers for the Tag
models we want to detach as our request data. The expected outcome of the DELETE
method is that these tags are detached - and any other already attached tags
are retained. Our database assertions check the database has been updated correctly.