مرور ایجاد پستها: نسخه بکاند0:00
Alright, let's do this. This is Captain's Log, February 2nd, 2981. We've been following along with the forum series for months now. We still seem to get no closer to creating posts. We can do everything with comments, but no sign of posts anywhere. The situation is looking pretty bleak, and I don't know what's going to happen if it carries on this way. Captain out. Alright, alright, I know. You've been so patient. Thank you for sticking around this long as we play with comments. And it's true, we can do all of this stuff with comments on posts, but we're still not in a position to actually be able to create a post. But now that we have all of the knowledge about using CRUD for comments, well, we can just apply that to posts, and it should be pretty straightforward. So in this episode, let's get a basic form together for being able to actually store a brand new post in
Adding Store Route1:09
just apply that to Post, and it should be pretty straightforward. So in this episode, let's get a basic form together for being able to actually store a brand new Post in our project. So under tests in our PostController directory, I've created a storeTest.php file ahead of time, and I've just filled out that very first test. It requires authentication, simply because it's almost identical to the tests that we've already created in previous episodes. I'll be doing that from here on out, so don't worry, just add that test in and then carry on. So I'm going to command T to run this test, and as we'd expect, the error is that that route is not defined. So by now we know the drill, jump into web.php and add that new route. It needs to be authenticated, so we'll put it inside here, and I'll do it as a route resource now that we're comfortable with route resources. And obviously it's a
and add that new route. It needs to be authenticated, so we'll put it inside here, and I'll do it as a route resource now that we're comfortable with route resources. And obviously it's a top level posts route resource that's going to go to the PostController. So we'll create the link, and the only route we want in there for now is the store route. Let's rerun the test and it passes. I'll tell you what, while we're here, why don't we refactor these two post routes that we have underneath to also use those fancy route resources. So we'll say route resource, and we still want posts, we still want it to go to the PostController, but we only want, well, the index and the show routes outside of our authentication scaffolding. Now we can remove those two lines, and let's go ahead and run everything in the PostController. Yeah, I would expect one to fail because that's inside our create test,
Writing Store Test2:38
scaffolding. Now we can remove those two lines, and let's go ahead and run everything in the PostController. Yeah, I would expect one to fail because that's inside our create test, which we've not come to yet, but all the others pass. I'm very happy that that is a successful refactor. And now that the route's in place, let's work on the logic to actually store a new Post in our database. So inside it stores a Post. We need a User first of all, because again, this is an authenticated route. So User::factory()->create(), and then we'll call actingAs to log this User in and make a post request to the post.store route as we have above. But this time we need to send some data across and the data will be obviously the content of our new Post. So a title, let's say "hello world" in this case. And what else do we need? We need some form of body as well, right? So did we call it body? Let's go to the Post
of our new post. So a title, let's say hello world in this case. And what else do we need? We need some form of body as well, right? So did we call it body? Let's go to the post table, have a quick look. Yeah, the column's called body. So we send over a parameter called body and let's say this is my very first post. That will suffice for now, completes our request. So the last thing to do in this test is assert that the database contains our new information. So we want to check the post table this time, and we want to pass in this information here. So rather than hard code and repeat myself, I'm going to extract this to a variable. Let's call it data and pass data in here, which actually means we could inline this request. And then here I'll spread data out. And the reason I'm spreading it out is I also want to check the user_id column to check that it's equal to the user_id from above. And
Implementing Store Logic4:12
And then here I'll spread data out. And the reason I'm spreading it out is I also want to check the user_id column to check that it's equal to the user_id from above. And if this is the case, well, I know that the Post has been configured correctly and assigned to the correct author. Okay, let's run this test. Obviously it fails. We'll jump into our PostController and make this pass. So here we have our store method, which is the perfect starting point. Once again, I'm going to do very basic validation here so that we don't end up removing code that we've written in just a couple of minutes, but I'll keep it to a bare minimum to follow TDD as closely as possible. So data equals request validate. This time we're dealing with two different keys, title and body. And title is going to have a simple required check for now, and body can also have a simple required check.
This time we're dealing with two different keys, title and body. And title is going to have a simple required check for now, and body can also have a simple required check. Both of those are correct rules anyway. You have to provide a title and a body to be able to publish a post. With our data in place, well, we can actually create a Post model. So let's say post. We'll use make for now. I'm going to pass the data in. And then obviously the data has some form of author or user. So we'll grab that and associate it with the request user. And then I can call save. Let's rerun our test. And it passes, as you can see down here at the bottom of the screen, meaning a post has been stored in our database and we can move on to our next test. So the next thing we want to check is that we redirect to the correct route, to the post show page. And thinking about it, well, 90% of this test
Redirecting After Store5:41
and we can move on to our next test. So the next thing we want to check is that we redirect to the correct route, to the post.show page. And thinking about it, well, 90% of this test above is correct. So I'll grab that information and we can just paste it down here. And I want to tag on an assertRedirect to the post.show route. And for now, let's put a hard coded one in there. So we're going to redirect to the Post with an ID of one. Let's run the test and see if we can make it pass. I'll jump back into our PostController and underneath this little section here, we can return to route and we'll return to the post.show route passing in, well, we need the instance of the Post. So let's assign this to a variable up here, and then we'll pass the post down here and run the test again. And it passes. Now we need to stop because what we've actually just done is introduced a huge bug into our
Fixing Save Return Bug6:26
up here, and then we'll pass the Post down here and run the test again. And it passes. Now we need to stop because what we've actually just done is introduced a huge bug into our application. But because our tests are passing, we might be confident in saying no, it is all fine. It's not all fine. Let's take a look at the problem. It's easy to miss when you're just slowly adding things and everything seems to carry on working. But the save method here does not return a Post instance, it returns a Boolean as to whether or not the save was successful. So what we've actually got here is true. The route helper receives true and tries to coerce the type into an integer. And in PHP, true is equal to one. So that is why the test passes because technically, yes, we've been redirected to a route of post.show with an ID of one. And we're imagining, well, that's the correct post in the database. The
So that is why the test passes because technically, yes, we've been redirected to a route of post.show with an ID of one. And we're imagining, well, that's the correct Post in the database. The fact of the matter is, we will always be redirected to a post with an ID of one, even if our post was actually ID 2000. I'm sure you'd agree that's a pretty nasty bug that has crept into our application. How can we avoid making mistakes like that? One thing I like to do is avoid using hard coded data that can make our tests brittle. Instead, I'll reach into the database and order by latest sorted by the ID, and then grab the first instance. So this is much less brittle, it doesn't matter how many posts end up being in our database, it will always grab the correct one, according to this test. Now, if we run this test, it will still pass, right, it's still passing. But check out what happens when we run all the tests in this
grab the correct one, according to this test. Now, if we run this test, it will still pass, right, it's still passing. But check out what happens when we run all the tests in this file. Now our test fails because it actually expected to be returned to a Post with an ID of two, not with an ID of one as is currently happening. Why? Well, MySQL auto increments the primary key in our post table. But Laravel doesn't instruct MySQL to reset that auto incrementing value between tests. So at the end of the test run, everything will be reset. And when you run a test again, yes, it's going to be back to one. But if you run tests in series, the auto incrementing integer, the primary keys, they'll just carry on from the last known value, even though the original posts have been deleted, which means that the actual new key, the new ID is going to be two, not one. And in this case, well, it's
last known value, even though the original posts have been deleted, which means that the actual new key, the new ID is going to be 2, not 1. And in this case, well, it's helped us pick up the fact that we actually have a bug in our application. What's the fix? Well, we just need to refactor the code that we have here to make more sense. So one thing we could do is drop out of this post early. And then we could say, post user associate save on a separate line. And now the test passes. Another thing we could do is use the syntax that we use for comments. So call create instead, and then spread this data out. And then we'll assign the user_id manually to be the request user_id, like so. And because we've called create, the creation process is going to happen in the same instance, and all three tests pass. So there is a great example of the problems that can arise when
we've called create, the creation process is going to happen in the same instance, and all three tests pass. So there is a great example of the problems that can arise when you make your tests brittle, when you rely on what you think should be correct, instead of actually just going and grabbing the real data from the database that is guaranteed to be correct. Also a lesson in making sure you check the return types of methods before you assume what they do. All right, let's get back to storing posts. Before we move on to our next test, I want to talk about this data array. We declare it here, we declared it here, and we're probably going to have to declare it in subsequent tests as well. We're repeating ourselves. And that's always indication that perhaps there's a little refactor that you'd like to do. In this case, why don't we extract this to a class level property.
Refactoring Validation Tests10:15
We're repeating ourselves. And that's always an indication that perhaps there's a little refactor that you'd like to do. In this case, why don't we extract this to a class level property. So I'm going to take this data array, and we'll come to the top of the test file here. I'll use a hook called beforeEach in order to be able to perform some action before each test executes. In this case, I'm going to set a property on the underlying TestCase, we'll call it validData, and it's just going to be equal to that array. And now we can get rid of this array. And instead, anytime we reach for data, well, we'll just reach for this validData instead. The outcome is the same, but now we have a single source of truth. Let's update this testValidData. Let's just run these two tests. Yeah, that one works. That one works. Very nice. I'm happy. Okay, let's now move on to checking.
of truth. Let's update this test valid data. Let's just run these two tests. Yeah, that one works. That one works. Very nice. I'm happy. Okay, let's now move on to checking that we have a valid title and a valid body. We want to perform more or less the same logic. So I'll go ahead and grab this here. And we want to pass in some bad data. So let's go ahead and spread out the valid data that we receive. But then I'll set the title property to the bad title that we're going to receive inside our function up here using the data set. And then we can assert invalid. And I want to check that there's a validation issue on the title property. Okay, so again, let's think about our data set. We're going to pass a null because it's required. It's got to be a string. So let's pass maybe a boolean again, an integer, a float. Let's also do str_repeat because we should have a maximum
a null because it's required. It's got to be a string. So let's pass maybe a Boolean again, an integer, a float. Let's also do str_repeat because we should have a maximum length on the title. I'm going to set a maximum length of 120 characters. So let's say 121 characters would cause an error. And I think we should also have a minimum number of characters for a title, maybe 10 characters. So if we say nine characters, then it should fail. Alright, let's run our data set test. And you can see we have six tests, one is passing. That's because we've already added the required check, five are failing. So let's jump back into our PostController. And let's make them pass. We should be able to add another validation check for string here. We said that we have a minimum number of characters of 10. And we have a maximum number of characters of 120. And now all six tests pass. Validation is so simple in
check for string here. We said that we have a minimum number of characters of 10. And we have a maximum number of characters of 120. And now all six tests pass. Validation is so simple in Laravel. It is crazy. I love the validation features. A validation check for the valid body is almost identical to valid title. So I'm just going to copy and paste that directly. Obviously, we'll change title to body. And I've got a different variable name here. So we'll update that as well. And the same is true with our dataSet data. So I'll grab that, paste it inside with, and all of this can remain the same, we just want to change the min and max values. So the maximum number of characters for a post should be 10,000. So let's say 10,000. And I'll say 10,001 because we want to go one above the maximum. By the way, it's a cool php trick, you can use an underscore to separate large numbers. It means exactly the same, but it's
say 10,001 because we want to go one above the maximum. By the way, it's a cool php trick, you can use an underscore to separate large numbers. It means exactly the same, but it's just visually easier to pass with your eyes. str_repeat, let's say you have to have 100 characters in the body. So we want 99 characters in our check here, run this. And again, as we'd expect, we have five failing tests and one passing. Let's jump into our PostController. This is a string, we're going to have a min of 100 and a max of 10,000. All right, all those tests pass as well. So storing a Post is pretty much there. But before we push any further, I just want to think about refactoring those validation tests to actually combine them in a single test will make it easier to add an update validation down the line. So at the moment, every time we add a new property or remove a property, we're going to have to come into this
single test will make it easier to add an update validation down the line. So at the moment, every time we add a new property or remove a property, we're going to have to come into this file and add a brand new test. But as we already mentioned, it's almost identical. So we could probably combine this. For example, what if we said it requires valid data. And instead of passing just a bad title, we could pass any bad data in, right, we also specify any errors that we'd expect to see inside our data set. So we're going to be passing two items in each test. Now we pass in our valid data as so but we're also going to spread out the bad data like this. Okay, so our array is going to be comprised of valid data, and then any overrides of bad data that we want to insert. And when we say assertInvalid, we're going to pass that array of errors to check for any issues that we'd expect to see. And the thing is, we could also pass a string in here. So let's
insert. And when we say assertInvalid, we're going to pass that array of errors to check for any issues that we'd expect to see. And the thing is, we could also pass a string in here. So let's update the types to say yeah, you can pass an array of errors if there is more than one. But you could also just pass a string. And for the dataSet items themselves, let's select all of these instances. And let's drop everything in an array so that we can add multiple items. And then we need our array of data. Remember, we're now converting from passing a single piece of information to a bad array of data. So in the case of our data here, well, all of these are going to be invalid titles. So we'll use an associative array to set that information. And then we want to say that the error we'd expect to see is on the title property. Let's run these tests again. And yet you can see they all pass. Hopefully you can see where I'm going here. We're now going to come down to this
error we'd expect to see is on the title property. Let's run these tests again. And yet you can see they all pass. Hopefully you can see where I'm going here. We're now going to come down to this data, we'll take it from this test, and we'll paste it underneath. Once again, we need to update the format. So we'll wrap it in an array, then we'll wrap the data in an array because we need to assign this the body property. And then finally, we'll say that the issue we're looking for in the validation errors is the body tag. Okay, let's run this, we should now have 12 passing tests, and we do, which means that anytime we want to come and add a new property to this form, it's as simple as adding a single line to this data set, we can now get rid of this test here. And we could probably actually even simplify the test body by inlining user factory create, because we never check the user again. Alright, how nice was that? Take the time to keep your
And we could probably actually even simplify the test body by inlining userFactory::create, because we never check the User again. Alright, how nice was that? Take the time to keep your test clean refactor, think about where you can improve things, because it will pay dividends months down the line when you need to come back and make additions or edits.
