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

Prefer integration tests0:00

Okay, today's question comes from Bart Jacobs on Facebook, and he says, I can't for the life of me figure out how to unit test my Eloquent models. Can you help? Yes, so here's what I would say. Don't try to unit test. If you're working with Eloquent, you're going to have to make a few assumptions, and one of those is that you're going to write integration tests, and the easiest way to do this is to actually hit a database, whether it's a database in memory or an SQLite database, anything you want, but that's going to be the easiest way, and don't let anyone tell you that it's in any pattern or anything like that. It's going to work out great for you. So let's try this. Let's create php artisan make:model. We're going to use this concept of a Notification, and I'll add -m to create the migration as well. Okay, so if we go to create notifications table, actually a quick little tip in Laravel 5.3,

Create model and migration0:40

We're going to use this concept of a notification, and I'll add -m to create the migration as well. Okay, so if we go to create notifications table, actually a quick little tip in Laravel 5.3, there's a whole new concept of notifications, but we're still on 5.2, so that's okay. We're going to set up a string for maybe we should do text to be safe. This will be the notification message, and then we're going to have another one that will be a Boolean, and this should indicate should you be a member in order to see the notification, or should you be signed in. So maybe must be logged in, maybe something like that. Okay, cool. So let's go ahead and migrate our database, and we now have our notifications table as well as, of course, the notification model. Okay, so next step. Imagine that we have a handful of methods here. For example, you want your API to be something like this, notificationPrepareMyMessage. We're going to have it link to

Add model API methods1:30

model. Okay, so next step. Imagine that we have a handful of methods here. For example, you want your API to be something like this, notification prepare my message. We're going to have it link to like a certain page on your website, some kind of URL. And behind the scenes, we're going to set it up so that prepare will instantiate a Notification class and set the message and anything else. The to method is a glorified setter that is essentially set link. In fact, we should probably add that as well, right? So let's go ahead and set that table string link, but we're not going to require that, and that can be nullable. Okay, php artisan migrate:refresh, and that'll roll everything back and rerun them. Okay, so we can get started on this. We know we want a static method called prepare that will accept the message and also maybe a Boolean that indicates whether or not you must be logged in, and we'll default that to true. Okay, well, we

want a static method called prepare that will accept the $message and also maybe a $mustBeLoggedIn Boolean that indicates whether or not you must be logged in, and we'll default that to true. Okay, well, we can return a new static and then just say compact $message and $mustBeLoggedIn. That's it. It doesn't persist anything. It just creates a new instance. We refer to this as a static constructor. Okay, the next one is going to be two, and this will accept the $url, and this is where we set the link. Like I said, just a glorified setter. All right, what else? Maybe we'll also have a query scope. So we'll have something like scopeLatest. So this would be when fetching your notifications, maybe you don't want to grab all of them. You just want to grab those from the last week. Maybe that's something you want to offer. Okay, well, we accept the $builder, or sometimes that's called $query. That's fine, and then we'll say $daysBack, and once again,

Maybe that's something you want to offer. Okay, well, we accept the builder, or sometimes that's called query. That's fine, and then we'll say daysBack, and once again, we'll default that to 1. Now you could say give me the query where the createdAt field is greater than Carbon::now() and go back this number of days. All right, and I will import that. Okay, so I think this is at least enough to get started. So you have a typical class like this, and you want to test it, but you're not exactly sure how to do this. How do you test a query scope? Well, once again, with unit testing, it's pretty awkward, and if you ever find yourself just throwing mocks everywhere, and your test method is complicated and confusing, you're doing it wrong instead, or you might be doing it wrong. I hate talking in absolutes, but in my opinion, that's not the way to go. So let me create a test for this. We're going to put it within our tests

Write integration model tests4:00

instead, or you might be doing it wrong. I hate talking in absolutes, but in my opinion, that's not the way to go. So let me create a test for this. We're going to put it within our tests directory, and generally what I do is I put it either within an Integration folder or at the very least a Models folder or both Integration Models. Now that doesn't exist. Let's go ahead and create it real quick. Integration Models. Okay, so now one more time. Test Integration Models. All right, so we have our test. We're going to have it extend Laravel's TestCase class, and screw all those documents. I hate that chunk. You can actually remove it, but I always forget to. Okay, so how do we go about testing this class? Okay, well, all you do is describe what it does. So think about this one. What does this method do? It prepares a notification. Simple. All right, let's try it out. It prepares a notification. And now here, just in terms of naming, this is what

So think about this one. What does this method do? It prepares a notification. Simple. All right, let's try it out. It prepares a notification. And now here, just in terms of naming, this is what I would recommend. You can do anything you want. So a lot of people would say testPreparesANotification. Eh, not a fan. A lot of people just call it by the name. So test and then the method name. I hate that. Yeah, you can do whatever you want, but this is my suggestion. I'm going to undo a few clicks. Use the test annotation so it gets read as a test. Then use underscores, and then make the name as long as it needs to be in order to describe what it does. It prepares a notification. Okay, so we're going to say notificationPrepareMessage, and I'll save that. Okay, really, that's how we can write out the logic for our first test. So what should now happen? Well, we know it's not being persisted, right? So we could just say this->assertEquals.

Okay, really, that's how we can write out the logic for our first test. So what should now happen? Well, we know it's not being persisted, right? So we could just say this assertEquals. At the very least, let's make sure that it assigns the proper values. So we could say a message, and then compare that against notification->message. All right, there's our first test. So we're going to run phpunit on tests/Integration/Models/NotificationTest.php, and it fails. So right off the bat, this ended up being useful for us. We get a mass assignment exception because here we're trying to mass assign, but of course we know that Laravel protects us against that. Let's set our fillable fields equal to message and mustBeLoggedIn to start. All right, we run it again, and now that works. All right, so good job. We're writing tests here. Let's do another one. This assertTrue, notification->mustBeLoggedIn. Remember,

All right, we run it again, and now that works. All right, so good job. We're writing tests here. Let's do another one. This assertTrue, notification must be logged in. Remember, we said that's the default, so this should return green, and it does. Sweet. Another one we might want to test is, well, let's make sure it didn't persist it. And one way you can do that is by saying assertFalse notification exists. So exists will return true if it has been persisted in the database. In our case, it hasn't actually been saved yet, so it does not exist, and we still get green. Good. You're writing tests here. What else does it do? Well, how about this one? It sets the notification URL. Okay, let's do it. It sets the URL, or how about the, yeah, I guess that's fine, the URL for the notification. All right, how do we do that? Well, once again, we could say notification->prepareMessage('example.com'), and now once again our test can be

that's fine, the URL for the notification. All right, how do we do that? Well, once again, we could say notification prepare a message to example.com, and now once again our test can be this assertEquals example.com, and compare that against notification link. All right, let's run it, and it fails. So once again, we spotted something that we did wrong. Okay, so what's the problem? Trying to get property of non-object. So notification is not an object at this point, which probably means our to method, yep, we're not returning anything. So we set the link, and then we return the instance, so that you could chain if you want, and that should return green, and it does. Okay, next, what about this one? It scopes queries to those within X number of days. Here's how we're going to do this. This is the one where we actually need to persist it. Up until this point, we could get away with not persisting a notification,

Use factories for data8:22

those within X number of days. Here's how we're going to do this. This is the one where we actually need to persist it. Up until this point, we could get away with not persisting a notification, but now we do. I'm going to go to database, factories, and we're going to set up a new one here. So this will be for our Notification. Think of a factory as, at least in this context, think of it as a blueprint for the model. So the blueprint for a Notification is a message that will be a fake sentence using the faker library, and then also must be logged in, and we could either set this to true, or you could also use faker.boolean, and that's just going to give you a random value. So now, because we've defined this factory, anywhere you can build one up. For example, if I do php artisan tinker, I could say factory for a Notification, and make me one. There we go. So this will populate all of the fields,

anywhere you can build one up. For example, if I do php artisan tinker, I could say factory for a notification, and make me one. There we go. So this will populate all of the fields, and it'll be different every single time. If you do want to persist it, you could call create, and that will save it to the database and then return. Cool stuff. So this becomes incredibly useful for your tests. Think about it like this. We're going to test this functionality, this query scope. So we write it out. It scopes queries to those created within the given number of days. Notice we make this method name as long as we need to in order to describe it. This is not an antipattern, and it's specifically, by the way, why we use underscores instead of camel casing. If you did camel casing, in my mind, it encourages you to keep those names short, and they don't need to be. Okay, so we're going to say, let's write this out. Assuming we have

casing. If you did camel casing, in my mind, it encourages you to keep those names short, and they don't need to be. Okay, so we're going to say, let's write this out. Assuming we have a Notification that is new, and also a Notification that is a week old, when I fetch all Notifications, but scope them to those from the last day, I should only get one result. Sometimes I do this even if I'm going to delete it right after. It's just sort of a way of charting out your game plan. Okay, assuming we have a Notification that is new, factory, Notification, class, and persist it. Okay, and also a Notification that is a week old. So we can do this. Let's set the createdAt to, and we'll use the Carbon library. Carbon::now()->subWeek(); So give me a timestamp for one week ago, and I will import that at the very top. All right, so now we have one record in the database, two records in the database.

sub week. So give me a timestamp for one week ago, and I will import that at the very top. All right, so now we have one record in the database, two records in the database. When I fetch all notifications, but scope them to those from the last day, so we would say notification, and how do we reference this? latest, and then we give number of days. Or we can stick with the default. All right, notification, latest, get. Finally, I should only get one result. Well, we know this should return a collection, right? Which is countable. So I could say assertCount(1), and compare that against the results. Okay, now when you're writing your tests, often you don't want to test everything. You just want to test the current method you're working on. So here's what I recommend. Copy the test method name, and then say phpunit, filter, and then paste it in. And now you're only testing that one alone, and nothing else. Okay, so argument.

working on. So here's what I recommend. Copy the test method name, and then say phpunit, filter, and then paste it in. And now you're only testing that one alone, and nothing else. Okay, so argument one should be an instance of QueryBuilder, but an instance of Eloquent. Okay, I'm sorry. I think I imported the wrong class. So if we come back here, yeah, we want Eloquent\Builder. Okay, run it again. Okay, so failed asserting that the actual size four matches the expected size of one. Now this is another one that trips up people so much. Here's the thing. When you're working with a database, by default, everything is not being reset. So I'll give you an example. We run it again. Now it sets it to five, and six, and seven, and eight. So the reason is, you are creating records in the database, but there's never a point when you clear them out, right? So every time you run the test, well, now there's more records in there. And of course, that's not going to match.

Reset database between tests12:37

records in the database, but there's never a point when you clear them out, right? So every time you run the test, well, now there's more records in there. And of course, that's not going to match up. Here's how you fix this. You're going to have, at the very top, use database transactions, like this. And why don't we go ahead and import that. So this is our way of saying, all of my tests are going to run through database transactions. And when the test is done, it's essentially going to roll everything back to its initial state. So now let's clear everything out. Why don't we just say php artisan migrate --refresh. And if we do phpunit again, we get green. And every time we're going to get green, because we roll back the database each time. Now here's another thing you might want to do. It's really easy to forget to add that part. So you could do one of two things. You could add use database transactions here within your TestCase class,

another thing you might want to do. It's really easy to forget to add that part. So you could do one of two things. You could add use DB transactions here within your test case class, or if there will be situations where you don't want to do that, instead, you could have this extend something like an IntegrationTestCase. And then you have use DB transactions on that. So yeah, it really just depends on how you want to go about it. Just don't forget to add that when you are working with the database. Okay, so in conclusion, what are the rules here? It's very, very simple. When you are testing your Eloquent models, hit the database, nobody's going to beat you up. And if somebody does want to beat you up, you can ignore them. You're working with ActiveRecord. So don't try to fight it every step of the way. If you try to mock things and say, well, this query should receive a call to the where method, and then a call to subDays,

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