-
-
Notifications
You must be signed in to change notification settings - Fork 247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(Yet Another) Assertions implementation #124
Conversation
Thanks for the effort! I love the implementation. How would you handle testing against resources and collections? So would something like this be possible? $projects = factory(Project::class, 5)->states(['public', 'published'])->create();
// snip
$response->assertPropValue('projects.data', ProjectResource::collection($projects->sortByDesc('published_at'))); I've tried myself to make a macro to handle this, but it is quite a pain. Especially when utilizing nested resources, like this: <?php
// snip
/** @extends JsonResource<\App\Models\Project> */
class ProjectResource extends JsonResource
{
public function toArray($request)
{
return [
'uuid' => $this->uuid,
'name' => $this->name,
'owner' => new UserResource($this->owner),
'images' => ImageResource::collection($this->images),
'urls' => [
'show' => 'demo.com',
],
];
}
} I just can not get a proper value assert against a resource collection like that. There is always an object/array typing difference between the prop value and the actual collection in the test. |
Interesting use case, I hadn't considered that one.. I'll take a look at it when have some time. Thanks for the feedback! |
Alright, so, after letting your question brew for a bit, I realized that normally my answer would be that "you're holding it wrong", because Eloquent API Resources are meant for exactly that: APIs. And since Inertia doesn't really have an API, I would instead recomment to look into using Eloquent's built-in However, at the same time, @reinink did support Eloquent API Resources as per 2b966ac, so I've still gone ahead and implemented it. Enjoy! Usage is as you'd expect: $response->assertInertiaHas('key', EloquentApiResource::collection([...]));
// or, for deeply nested values
$response->assertInertiaHas('deeply.nested.key', EloquentApiResource::make(...)); |
Thank you @claudiodekker ! I will test it out tomorrow (hopefully). The reason why I love API Resources so much is because you can create different resources for the same model for different situations, conditionally add data, etc. For example, you can create a ProjectIndexResource for a list of projects and a ProjectResource that is used everywhere else. And if your project has an API, you can add them there as well. Win-win. It's just easy to use and extremely flexible compared to the the all-or-nothing |
Reporting back after testing. It seems to work great for single-depth resources. But if I start nesting resources, I can not really get the assertions to work. Could you tell me if I am doing something wrong @claudiodekker ? I'll try to be as detailed as possible. This is what it looks like in the Vue inspector: This is what the UserResource looks like: namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->name,
'email' => $this->email,
'roles' => RoleResource::collection($this->whenLoaded('roles')),
];
}
} And the RoleResource: namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class RoleResource extends JsonResource
{
public function toArray($request)
{
return [
'name' => $this->name,
];
}
} Finally, the controller: class ProfileController extends Controller
{
public function index()
{
return Inertia::render('Profile/Index', [
'user' => new UserResource(Auth::user()->load('roles')),
]);
} There is a user object with a roles array inside it. So I assumed this would work: But I get the following error: When I try to dump the roles using So I think there is still an issue with nested collections. This is the test I used: /** @test */
public function shows_the_current_user(): void
{
$user = factory(User::class)->state('user')->create(['name' => 'Test Name'])->load('roles');
$response = $this
->actingAs($user)
->get(route('profile.index'))
->assertSee('Test Name');
$response->assertInertia(); // == OK
$response->assertInertiaComponent('Profile/Index'); // == OK
$response->assertInertiaHas('user', new UserResource($user)); // == OK
$response->assertInertiaHas('user.roles', RoleResource::collection($user->roles)); // == Error
dump($response->inertiaProps()['user']['roles']); // == Error
} I also do not really understand why the P.s. to test this code I dropped my own code to add assertions and noticed an assertInertiaCount was missing. I think this would be a useful addition. |
Alright, so, I looked into this for you. Here's what's happening: The problemThe error you're getting (
Basically, what happens, is that Laravel looks for the If we then return/take a step back up the call-stack, you can see that this 'data'-value gets used as the key in a new array, where it's value is the existing instance data (read: the data is wrapped). This then again gets returned, and eventually gets send to the browser as a 'Response' object. However, since Inertia makes this call instead, it then has a Fix number 1 (in your code)This call: $response->assertInertiaHas('user.roles', RoleResource::collection($user->roles)); Should actually be: $response->assertInertiaHas('user.data.roles', RoleResource::collection($user->roles)); Because the prop you set, is only 'user'. Everything below that gets 'wrapped' in the data key by the Resource: return Inertia::render('Profile/Index', [
'user' => new UserResource(Auth::user()->load('roles')),
]); However, doing this, while more correct, still wouldn't make ANY behavioural difference. Here's why: If we take a look at that Since this PR (as well as Laravel's This also explains the other error you're experiencing: dump($response->inertiaProps()['user']->data->roles); "Fix" number 2 (in Inertia.. Spoiler alert: Not needed.)The solution appears to be to change the way Inertia itself prepares - $prop = $prop->toResponse($request)->getData();
+ $prop = $prop->toResponse($request)->getData(true); That way, the However, even if we make this change, it only places us in an entirely different problem space:Due to the serialization that happens, contexts gets lost, so, doing something like this: $response->assertInertiaHas('example.data.children', $children); will give you the following error: Alternatively, if we try to match against a child resource that is a $response->assertInertiaHas('example.data.children', UserResource::collection($children)); We'll still get this error, because of the wrapping that I explained earlier: But wait..Since we're asserting against what is essentially a deeply-nested array, shouldn't the top-level comparison already take care of this? Because if that's the case, we don't even need to assert against (deeply) nested resources/collections: And it appears it does! You can validate this behaviour yourself, using a slightly modified example of your own test: /** @test */
public function shows_the_current_user(): void
{
$user = factory(User::class)->state('user')->create(['name' => 'Test Name']);
// Let's fetch the objects twice, so that we that they're reliable test subjects:
$userA = User::find($user->id)->load('roles');
$userB = User::find($user->id);
$response = $this
->actingAs($user)
->get(route('profile.index'))
->assertSee('Test Name');
$response->assertInertia(); // == OK
$response->assertInertiaComponent('Profile/Index'); // == OK
$response->assertInertiaHas('user', new UserResource($userA)); // == OK
$response->assertInertiaHas('user', new UserResource($userB)); // == Error ('roles' not loaded)
dump($response->inertiaProps()['user']->data->roles); // == OK
} |
# Conflicts: # composer.json
Amazing! Thanks for the work @claudiodekker ! I really hope it gets merged! |
@johanvanhelden For now, you can require this package instead of using this fork. |
Oh, sounds good, thanks for making a package! |
Good call making it a package, thanks @claudiodekker! I added it to |
@claudiodekker since you seem to be working more closely with @reinink , do you know if there are any plans to make this part of the core now? |
Hi @johanvanhelden, that's true, but for now we're fairly comfortable with keeping this a separate package, which you can consider semi-official and can expect to remain up to date with Inertia's changes. That said, if and when we make this part of Inertia's core, I'll let you know by adding a message to this issue. |
Superseded by #220 |
Motivation
Like others, I've been in search of a way to test the backend outputs towards an Inertia view. While Macros are the obvious solution that others have also already discovered & suggested via PR, there were some things there that just didn't feel 'right' to me.
For example, while I do believe #51 to be a great implementation, I wasn't a big fan of the idea of having to swap out the
TestCase
class in our own projects. Similarly, while the suggestions made in #105 aren't bad either, but having to runInertia::fake()
was kind of a dealbreaker to me.So, here's my attempt.
Usage
This PR suggests a couple of assertion helpers that are available out of the box once you update to whatever version this PR ships in. To use, simply chain them on your
TestResponse
responses.Available Methods
The methods made available by this PR closely reflect those available in Laravel itself.
(In fact, I just got a notification that a few changes I suggested were just merged into the framework)
Assert whether the given page is an Inertia-rendered view
Return all the available Inertia props for the page
Assert whether the Inertia-rendered view has a specific property set
Apart from checking whether the property is set, the same method can be used to assert that the values match
It's also possible to assert directly against a Laravel Model (or any other
Arrayable
orResponsable
class)It's also possible to check against a closure
Next, you can also check against a whole array of properties. It'll simply loop over them using the
assertInertiaHas
method described above:Finally, you can assert that a property was not set:
Details & (Breaking) Changes
This PR bootstraps the Macros directly onto the
TestResponse
from the ServiceProvider, and only does so if we're running in a testing environment.I've also made it compatible with Laravel 6 and earlier, as that's where the
TestResponse
namespace was changed fromIlluminate\Foundation\Testing\TestResponse
toIlluminate\Testing\TestResponse
.Finally, I've changed the package constraints of this repo to be only compatible with Laravel 5.4 and up, because Laravel 5.3 depends upon
phpunit/phpunit: ~5.4
, which was when the non-namespaced PHPUnit classes (e.g.PHPUnit_Framework_Assert
) were still used. (Laravel 5.4 changed this).While this might seem like a breaking change, I'm fairly confident it isn't, as on those versions other things in this package already likely wouldn't have worked 😅
Let me know if this is something you're happy with, or whether you want me to tweak certain things.
(Or, alternatively, there's no hard feelings if you decide to just decline this PR altogether, so feel free to do so..)