تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Designing Resource Assertions0:00

Right, where were we with our tests again? Oh yeah, that's right, it passes posts to the Vue. And currently we're just checking that a property called posts has been given to the frontend, which of course passes, but this is not enough to give us confidence that everything works correctly. Now I want to stop and just think about what we want to create here, because we're going to be performing this check in many different tests across the series. We're going to be checking that the right data is being given to the frontend. And we're always going to be wrapping our Eloquent models in JSON resources or JSON collections. So I want to find a way, figure out a nice syntax that will allow us to very quickly perform that check without having to jump into all the ins and outs of properties and IDs.

So I want to find a way, figure out a nice syntax that will allow us to very quickly perform that check without having to jump into all the ins and outs of properties and IDs and checking the titles match and all of that, because I trust that Laravel is doing that part correctly. I don't want to test the framework, I just want to test my code. So let's come up with an ideal syntax for the test we want to write. Imagine if instead of has, we could say hasResource. And I'm just going to deal with a single resource first, and then we'll move on to paginated collections. So you have a resource called Post, and you expect that that resource is equal to a PostResource.

and then we'll move on to paginated collections. So you have a resource called Post, and you expect that that resource is equal to a Post resource. And then obviously we need to pass in the model that we want to check against. So we'll create a few posts up here. Let's say posts equals PostFactory::create(). We'll create three of them and then call the create method. And by the way, only ever create as much data as required for a test and no more, because it takes time to actually build those models. It obviously has to hit the database each and every time. So if you needlessly create tons of data in your tests, your test suite will slow down,

It obviously has to hit the database each and every time. So if you needlessly create tons of data in your tests, your test suite will slow down, and you'll find that rather annoying as your test suite grows. Only create the minimum amount of data necessary. Let's go ahead and grab the first Post in our collection. And that's basically all I want to write for this test to pass. I want everything else to happen under the hood. Of course, phpstorm squawking because I don't have a method on the assertable Inertia object called hasResource. This is what we would want to happen, but it isn't going to happen just yet. The first thing I actually want to do is make sure that this test could pass if it existed.

This is what we would want to happen, but it isn't going to happen just yet. The first thing I actually want to do is make sure that this test could pass if it existed. So I'm going to go ahead and create a little post property here, which is just PostResource, and we'll call the make method, and we'll just grab the first post in the database like so. So everything's set up for this to pass in an ideal world. How can we actually add this method to assertableInertia? Well, thankfully, assertableInertia extends assertableJson, and assertableJson is macroable, which is Laravel's way of allowing us to magically extend objects,

Implementing hasResource Macro2:52

and assertable JSON is macroable, which is Laravel's way of allowing us to magically extend objects, and it's perfect for writing these test helpers. So at the top of the test here, for now, we'll extract this into another place shortly, but for now, at the top of the test, I'm going to say assertable inertia, call the macro method, and I want to provide the name of this function, which is hasResource. Then I provide a closure, which is going to receive the two parameters that we pass here. So the first one is a key, string key, and the second is the resource, which is always going to be an instance of a JSON resource.

So the first one is a key, string key, and the second is the resource, which is always going to be an instance of a JSON resource. There we go. Now, inside this method, I need to perform all of the checks, the logic, to make sure that it is the correct resource. So the first thing I want to do is grab the properties that have actually been passed down to inertia. Let's extract those to a variable, and this, in this instance, is equal to assertableInertia because we're inside the macro here.

and this, in this instance, is equal to assertableInertia because we're inside the macro here. I hope that makes sense. It can sometimes be confusing, but basically, under the hood, Laravel is binding the this keyword to the assertableInertia instance that you're working on. So I think this has a toArray method. Let's just go ahead and dump and die out the value of that and run the test. Yeah, here we go. Here is the toArray of the inertia component, and here are the props.

Yeah, here we go. Here is the toArray of the Inertia component, and here are the props. So I want to go ahead and grab all of those props. Let's get rid of that a second, and we'll grab the props property, and now we're dealing just with the properties that have been passed to the front end. Now, I can use Pest expectation API to do the rest of my checks here, but you'll want to reach for assertions if you're using PHPUnit. So I'm going to grab those properties, and first of all, we want to make sure that we actually have the specified key here inside the properties passed down to Inertia.

and first of all, we want to make sure that we actually have the specified key here inside the properties passed down to Inertia. So let's say toHaveKey, and we'll check for that key in those props, and if you haven't passed it, well, we'll give a custom message. So we'll say key, and then we'll obviously output the given key. Not passed as a property. Oh, if I could spell. Property to Inertia. Okay, should we see if that works just as it is out of the box? If I go back to my PostController and I comment out this single post

Okay, should we see if that works just as it is out of the box? If I go back to my PostController and I comment out this single post and we run our test again, there we go. Key post not passed as a property to Inertia, but if I bring it back, hopefully, yeah, now the test passes. So our first check is working correctly. What else do we want to check? Well, now we need to grab the actual key that we've specified, so the key that we've given here, and we need to check that it's equal to this JSON resource.

so the key that we've given here, and we need to check that it's equal to this JSON resource once this JSON resource has been compiled. That is, once it's been transformed from an object into the associative array that will be passed to the front end. Now, in order to compile it, let's create a variable called compiledResource, and we can say resource, and there's a response method on here. Let's have a look at what that does. Transform the resource into a HTTP response. So let's go ahead and do that, and we'll dump and die out the value.

Transform the resource into a HTTP response. So let's go ahead and do that, and we'll dump and die out the value so we can see what that looks like. Yeah, sure enough, we have a JSON response, and down here somewhere in the content, you can see we have the output of that resource as a piece of JSON. Now, on a response object, I believe there's a getData property, which we can pass true to return an associative array. So now we literally have the output that is being passed down to the front end once the resource has been compiled.

So now we literally have the output that is being passed down to the front end once the resource has been compiled, and that is a perfect starting point for comparison. So I'll get rid of the dd. We now have a compiled resource, and we should be able to jump one step further and grab that key from the props and assert that it's equal to the compiled resource like so. And I'm just going to return this so that we can chain things to the end of our assert Inertia chain here.

And I'm just going to return this so that we can chain things to the end of our assert Inertia chain here. Okay, let's run our test, and it passes. Brilliant. Let's make sure it's actually working by going back to our PostController, and let's get rid of the wrapping. So we're going to get rid of postResource, and we'll run our test again. Now the test fails because the arrays that have been passed down to the front end are different.

Now the test fails because the arrays that have been passed down to the front end are different because if you just pass a Post, it also includes the User ID. We haven't specified how to transform it into JSON, so the model gets transformed automatically and includes those extra details as well. So now we have this really cool hasResource macro that we can use to check any given resource wrapping a single Eloquent model.

Adding Paginated Resource Macro7:35

that we can use to check any given resource wrapping a single Eloquent model. But of course, in many cases, we don't have a single Eloquent model. We have a paginated collection of models wrapped in a resource. So let's create another macro that we can use for those instances. Again, first things first, I'm going to create my ideal syntax. So maybe we call it hasPaginatedResource, and we look for posts rather than post

So maybe we call it hasPaginatedResource, and we look for posts rather than post because in PostController, that's obviously the name of the key here. And rather than PostResource make, we want to collect all of the posts that we create at the top of the test. Okay, that looks pretty nice. As a starting point, we could go ahead and duplicate what we have here.

As a starting point, we could go ahead and duplicate what we have here. So we'll just duplicate and we'll paste it underneath. We change the method name to hasPaginatedResource, and we're no longer dealing with a JSON resource. We're dealing with a resource collection, so slight difference there. We still want to grab the data from Inertia, and we still want to compile the final resource here. Let's just take a look at what that compiled resource looks like.

and we still want to compile the final resource here. Let's just take a look at what that compiled resource looks like. There we go. We have an array of finalized data, and I'm pretty sure if we were to take a look at the extracted property, which is this one here, so we'll dump and die that value out. Yeah, look at that. We have the data key, and we also have the links key and the meta key.

We have the data key, and we also have the links key and the meta key. So that's how we know that this is a paginated resource. So why don't we go ahead and say to have keys, and we'll check for data, we'll check for links, and we'll check for meta. And we're going to assume that if those keys exist in the data pass to Inertia, it is indeed paginated. We could go deeper and we could check for the specific subkeys inside those arrays,

We could go deeper and we could check for the specific subkeys inside those arrays, but again, at that point, you're kind of testing the framework. This is enough for me, at least, to check that, yeah, we are in a paginated resource, and things are being passed down correctly. Once we have those keys, I actually want to dive into that data property because it's the data property that will contain the compiled resource information.

because it's the data property that will contain the compiled resource information. And I'm going to check it against that compiled resource. So very much the same as the hasResource method we created, the main difference being that we dive one step deeper before doing the comparison, and we're also checking for the links, meta, and data keys. Let's run the test, and it's passing. Shall we see if it's really passing? Let's go back into our PostController,

Fixing Ordering in Tests10:09

Shall we see if it's really passing? Let's go back into our PostController, and I'm going to update this slightly. I'm going to update this to grab the latest first, so it will check the created_at timestamp, and it's going to order by that descending. Now, the problem is with our test data, if we run this, we still get a passing test, and we've not changed anything here. I would expect that I'd actually have to reverse this array,

and we've not changed anything here. I would expect that I'd actually have to reverse this array, but obviously when we run that, it fails. And the issue is here, the Post that we create using our factory all have the same created_at timestamp. Now, that puts us in a slightly awkward situation because the situation is two Posts get created at the same time, which should come first in the index. In my mind, not only should we order

which should come first in the index. In my mind, not only should we order by the created_at column descending, but we should also order by the ID column descending. So, let's go ahead and do that too. I'll call latest again, but this time I'm going to specify the column of ID. And if we dd the SQL here, and then rerun our test, you should see select * from posts,

and then rerun our test, you should see select * from posts, we order by created_at first, and then we order by id. That's perfect. That's pretty much exactly what we want. And if we run our test once more, we now have a passing test. But if we pass the posts in the original order, then it fails.

But if we pass the posts in the original order, then it fails. And that is perfect because that means we have a gorgeous syntax, hasPaginatedResource for very quickly checking that the data matches. And we're not having to do additional database queries inside our test to grab this data. We're just passing in the data that we create up here at the start of the test.

Extracting Macros to Provider11:50

We're just passing in the data that we create up here at the start of the test. I really like this syntax. I think it's going to be perfect for our needs going forwards. Now, we can't finish this episode without extracting these macros outside of the test because currently these two functions will only work in this one test. We need to extract these to a service provider.

will only work in this one test. We need to extract these to a service provider. I'm going to go into the terminal here and I'll say php artisan make:provider. And let's call this the TestingServiceProvider. Let's open up that TestingServiceProvider. And in the boot method down here, I want to actually check if we are running unit tests. If we're not, then I want to finish early. So let's use an if statement for this.

If we're not, then I want to finish early. So let's use an if statement for this. And the check will be this app and running unit test exists. And I'm going to use the not statement because if we're not running unit tests, then I'll just return early like so. So everything under here will only take place if we are running unit tests. Okay, with that done,

if we are running unit tests. Okay, with that done, we should be able to simply come into our code here and we can take these two macros that we created and we can just paste them directly into our brand new ServiceProvider like so. The last thing we need to do is register this ServiceProvider. So I'll go into config/app.php and there should be a providers key. Here we go. Here's providers.

and there should be a providers key. Here we go. Here's providers. And then down here we can add app, providers, and we're looking for the TestingServiceProvider that we just created. Awesome. Shall we see if our test still passes? Hopefully it does. There we go. It still passes.

Hopefully it does. There we go. It still passes. And if you're using the Laravel IDEA plugin for phpStorm, you should be able to generate helper code. And in just a quick moment, it should actually find this macro using your service provider. There we go. It's recognized it. So now we have autocomplete for hasResource and hasPaginatedResource.

Adding TestResponse Assertions13:46

So now we have autocomplete for hasResource and hasPaginatedResource. How cool is that? Pretty nice. So this is great. I'm really happy with this syntax. We are going to be using this code a lot. So I'd like to be able to call it at the top level of a GET request. In other words, I want to be able to do this, but here.

In other words, I want to be able to do this, but here. hasResource, hasPaginatedResource. And maybe to differentiate our prefix with assert. So assert hasResource. assert hasPaginatedResource. This should actually be pretty straightforward to add. We just need to add a couple more macros to this time the testResponse, which is what is returned from this method here.

to this time the test response, which is what is returned from this method here. So anytime you make a request in your tests, you get a test response back. And this is also macroable. So we can say assertHasResource. And again, what do we receive? We receive a key and we receive a JSON resource, which is going to basically just defer to Inertia. So this assertInertia,

which is going to basically just defer to Inertia. So this assertInertia, which receives a closure with assertableInertia in it. And that is simply going to defer to hasResource, passing in the key and the given resource as well. And it will be returned from the macro. So that's our assertHasResource. We also had assertHasPaginatedResource. So let's add that. assertHasPaginatedResource.

So let's add that. Assert hasPaginatedResource. This is a resource collection, as we mentioned previously. And instead of calling hasResource, I want to call hasPaginatedResource. Okay, our macros are in place. Again, I'm going to get Laravel Idea to generate helper code so that I have autocomplete for these two methods. And hopefully now in our test, we can remove this code here.

And hopefully now in our test, we can remove this code here. And we're left with a very streamlined test. We get the post index. We assert that the post property is equal to this resource. We assert that our paginated resource of posts is equal to this post resource collection. I can actually remove this because in my PostController, I'm actually going to be passing a single post down to the front end. But we have that method going forward.

I'm actually going to be passing a single Post down to the front end. But we have that method going forward anytime we do need to pass a single resource, which we will very shortly for our post show page. Finally, before we wrap up, I always think it's important once you've created a working implementation that you go back and refactor to clean things up and make it nice and easy to maintain in the future. And I want to do that for our two macros.

and make it nice and easy to maintain in the future. And I want to do that for our two macros. The first thing that sticks out to me is this line here. Why are we grabbing all of the properties passed down to Inertia when we're only interested in this one here? So I imagine on AssertableInertia, there is a method. Yeah, what about prop? There's a method called prop, which receives a prop within the current scope.

There's a method called prop, which receives a prop within the current scope. Now, it's protected, but that doesn't matter because we're inside the AssertableInertia instance thanks to how macros work. So we could actually replace this code here with this prop, and we pass in the key like so. And now we also use props here to check if the key exists, but, well, Inertia already has a method for that. This has passing in the key,

but, well, Inertia already has a method for that. This has passing in the key, so we can use that logic instead. That allows us to remove this and have a much simpler expect chain. And this is only used in one place, so why don't we inline this code here? We can now get rid of this props array, and we're left with a very simple method that will be super easy to update.

and we're left with a very simple method that will be super easy to update. What about hasPaginatedResource? Well, other than the fact that we're checking the keys exist, everything else is the same. The only real difference is that we jump down into the data property after grabbing the original key. So maybe we can defer to this method with a little bit of extra logic. We could say this hasResource.

with a little bit of extra logic. We could say this hasResource. We pass in our key, but we want to modify it slightly and add .data, seen as the has method and the prop method support .syntax. Obviously, we'll pass in the resource that we've provided as well so that that can happen. And now we can get rid of this. We can get rid of this line. Rather than expecting props,

We can get rid of this line. Rather than expecting props, we can say this prop, and we can pass in the key, and then we can get rid of everything other than to have keys in order to check that it is actually paginated. And let's move that onto a single line again. Once more, we have a very simplified refactored hasPaginatedResource method.

Once more, we have a very simplified refactored hasPaginatedResource method that's going to be nice and easy to update. Let's just test that everything still works. We'll run this test again, and we're passing with flying colors. Nice. I'm very happy with this. I think this puts us in a great position to move forward, and it's going to make writing these tests and ensuring that we are giving the right data.

and it's going to make writing these tests and ensuring that we are giving the right data to the front end that much simpler. So important that you just take the time to write these testing helpers because they save you so much time in the long run, and they're very easy to tweak if you need to make changes down the line. Okay. Speaking of that Post show page, we kind of need that in place

Okay. Speaking of that post show page, we kind of need that in place before we can move anywhere else, so why don't we take a look at building that out in our next episode.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟