مرور رویکردها، اصطلاحات و ملاحظات تست0:39
its own conferences. So, yeah, if you want to be frank, we could dedicate course after course after course after course to teaching you the ins and the outs and the nuts and bolts of testing. And we just don't have time for that, and it's beyond the scope of a beginner series like this. Nonetheless, I do want to give you one more episode where we discuss everything from the general approach, the different styles of testing, what you can expect, what it looks like, all of that stuff. Okay, let's get going. Yeah, of course, in the last episode, near the end, we installed PEST, and we now have
Unit vs Feature Tests1:09
Okay, let's get going. Yeah, of course, in the last episode, near the end, we installed PEST, and we now have this new test directory. And I want you to notice how it's categorized into these different styles of testing. One is called feature, and one is called unit. You know, these days, I'm not even sure if it's that useful to categorize things in this way. But nonetheless, it's good to understand the basic idea. Unit represents, well, one unit of your code base. That unit could be a single class, a single function, or a small collection of classes.
Unit represents, well, one unit of your code base. That unit could be a single class, a single function, or a small collection of classes. But still notice how they comprise a single, small, typically, unit. On the other hand, feature test refers to something much wider, a feature in your application. That feature could be, what would be an example from Laracast? Oh, here's one. I just built a referral system for Laracast. That's a feature. So, we could have a feature test called ReferralTest. And within there, I can describe the rules of a referral system.
Using Tests to Design2:08
So, we could have a feature test called ReferralTest. And within there, I can describe the rules of a referral system. What does it consist of? What are you allowed to do? What are you not allowed to do? So, from that perspective, we might realize that when we write tests, it's not just to ensure the thing works, even though that's very useful. But it's also almost like a scratch pad. It's a great way to gather your thoughts. For example, let's go into ExampleTest here and rename this to ReferralTest.
It's a great way to gather your thoughts. For example, let's go into exampleTest here and rename this to referralTest. We're not going to build this out. But just to give you an idea of the approach, you might begin by writing the various features as tests. So, we might have something like... And by the way, we can use this test function or it. They are aliases. Use whichever one best fits your description for the feature. So, for example, it allows subscribers to earn money by referring their friends.
Use whichever one best fits your description for the feature. So, for example, it allows subscribers to earn money by referring their friends. Okay. So, yeah. In this case, this is very top level, very zoomed out. But still, it would be a fine test to write. It might be fairly in depth. For example, you might write the test in such a way where you create a User. You sign the User in. You generate a referral code for that User.
You sign the User in. You generate a referral code for that User. And then you create another User who redeems that referral code. And when they do redeem that code, and by the way, maybe when we speak it out and we describe it here, we realize, oh, redeem is a good verb and that should be referenced or represented in the code base somewhere. So, maybe if you have a Referral class, then maybe you have a method called redeem. So, again, this is what I mean when I say it's almost like a scratch pad. It gives us a way to gather our thoughts and then start identifying patterns or keywords that we can then work into the code base.
Test-First vs Test-After4:53
So, for example, if writing these tests before your implementation code just doesn't work for you, no matter how hard you try, don't do it. And by the way, that's referred to as test first development or test driven development. On the other hand, you might find situations where it makes sense in this context, but not in that. It's too hard to wrap your mind around the flow. But over here, it makes perfect sense because you have explicit input and output expectations. It doesn't matter. There's no rules. The only rule is that you make this work for you.
It doesn't matter. You decide what the rules are for your code base and your feature, and then you write them out, and then you write the test to ensure that it does what you expect. Okay. Cool. Next, we have this container test that we worked on in the last episode, which is this incredibly real world. This represents a simple unit test that we can resolve something out of the container. But now, do we have anything else we might want to work on here? We could test some middleware.
Writing Validator Unit Tests7:08
But now, do we have anything else we might want to work on here? We could test some middleware. We could test the validator. These are really just simple static functions. But you know what? I think this would make sense. So, let's give it a shot. Let's create a new test, and we're going to call it validatorTest.php. All right. So, why don't we say it validates a string.
All right. So, why don't we say it validates a string. All right. So, let's pull in our class. We're going to call the string method, and you'll remember, if we quickly switch back, we give it the value, and then an optional minimum and maximum character count. Okay. So, let's switch back, and we'll say foobar. Okay. So, if we run this, should this be valid?
Okay. So, if we run this, should this be valid? Is foobar a string? Absolutely. So, let's save that to results, and then say, expect result to be true. So, I could do this, or I could use the method to be true. All right. Let's give it a run. So, I could do it directly from my editor, because I'm using PhpStorm, and really, most editors these days will have some kind of inline support.
So, I could do it directly from my editor, because I'm using PhpStorm, and really, most editors these days will have some kind of inline support. But yeah, for now, let's just do it directly from the terminal. So, I will run test on the TestUnit, ValidatorTest, and of course, it returns green. Okay. Now, I'll give you a little tip, though. Let's do this. Let's inline this variable entirely, and we could do another one. So, for example, if I said false, I would expect that to be false, and if we give it a run, we already know that's going to work.
So, for example, if I said false, I would expect that to be false, and if we give it a run, we already know that's going to work. Let's do another one, and this time, why don't we feed it an empty string, and that should return false, because it's empty. Give it a run, and that works. Okay. We could add another one here, and we'll clean this up and refactor shortly. It validates a string with a minimum length. So, for example, if I run this, but I say the minimum length should be 20 characters, well, in this case, foobar is not 20 characters, so I expect that to return false.
Both work. It doesn't matter. Do what you want. Okay. Cool. Let's do another one for email, and then we'll clean things up. So, we'll say it validates an email. All right? So, if I call email with foobar, I will expect that to not be valid. Give it a run.
So, if I call email with foobar, I will expect that to not be valid. Give it a run. It works. If I give it an actual email address, or a dummy email address, in that case, it should return true. Give it a run, and oh, it doesn't work. Failed asserting that foobar is true. Yes, of course. So, already the test is informing us that our assumptions aren't exactly what the code says.
Adding Types and Returns10:27
So, already the test is informing us that our assumptions aren't exactly what the code says. So, in this case, I was assuming it returns a Boolean, but instead, it returns either false, if it is not an email address, or the value, if it is. So, now we have to decide, are we okay with that, or do we always want it to return a Boolean? So, for example, do we want to force it to be a Boolean? Well, if we take that, I think it will fix it, and yeah, that does solve the problem, but again, this is a decision that you have to make. Does validateEmail return a Boolean, or does it return false, or the email itself?
but again, this is a decision that you have to make. Does validateEmail return a Boolean, or does it return false, or the email itself? You have to decide that. And a quick note, we haven't talked about this very much, but you can leverage types to be a little more explicit about the types of input that you expect, and again, we haven't talked about this very much, because it's not a high level topic, but only because I think there is a beauty in teaching the basics of PHP in a very dynamic way. That's the way I learned it, and I feel like it's just easier to grasp when you are not constantly throwing syntax at the viewer, at the person who is trying to figure out how all of this works, but nonetheless, it can be incredibly useful.
constantly throwing syntax at the viewer, at the person who is trying to figure out how all of this works, but nonetheless, it can be incredibly useful. So a quick little 30-second primer, if I want to be explicit that this parameter should be of type string, then I could write string here, and now if we were to call this email method and feed it an array or an object or a class or something like that, it's going to fail, and it will let you know, hey, you gave us an array, but we thought you were going to give us a string, and we expected you to give us a string, so maybe you need to take a look at this. And then we can also do return types represented by a colon and then the type. So in this case, if I want a Boolean, and actually take a look at this, so real quick,
And then we can also do return types represented by a colon and then the type. So in this case, if I want a Boolean, and actually take a look at this, so real quick, without the type, if we give it a run, it fails because we're expecting a Boolean in response. So one fix was to manually cast it like this, and that works. However, another option is to declare a return type of a Boolean. So now notice if I run it, that fixes the problem too. So this is teaching us that when we declare a return type, if what we return doesn't match that type, it will automatically be cast in the best way possible to become that type. You can disable that if you want, if you want to be a little more strict, but nonetheless,
and the pros and the cons and the benefits, and I'm repeating myself at this point. Let's keep going. So now why don't we wrap up by maybe doing one more. So let's do this. Let's go ahead and import this at the top, and that way we can simplify these. And why don't we validate, I don't know, it validates that a number is greater than a given amount or something like that. So we could say greaterThan, and we'll say we need to give it the value. So maybe the value is 10, and we expect that to be greater than 1. Well, if we run that, that should return true.
TDD: Implement greaterThan14:03
So maybe the value is 10, and we expect that to be greater than 1. Well, if we run that, that should return true. Okay, so let's do this. I'm going to call this method only using pest, and that declares that, well, you can skip all of these right now. I'm only interested in this one test alone. So we give it a run, and notice we only have one test here, and it failed, of course. Call to undefined method validator greaterThan. Okay, so now we're taking a test first approach. We're seeing a test failure, and that is guiding us to write implementation code.
Okay, so now we're taking a test first approach. We're seeing a test failure, and that is guiding us to write implementation code. So let's do that now. We go into the Validator. I create a new static method, and we're going to call this greaterThan. This will accept, and we'll add a type this time, a value, and then a greaterThanAmount, and that will be an integer. Okay, let's give it a run. Now, it's still going to fail, but we should change the failure message. And again, this is sort of a cornerstone, a bullet point of test first development.
Now, it's still going to fail, but we should change the failure message. And again, this is sort of a cornerstone, a bullet point of test first development. You want to make that error message change. So even though you're getting false, if you can change the error message, ideally, you're making progress, and that's the case here. Alright, so now the method name exists, but it's returning null when we expected true to be returned. Okay, so let's do this. Let's be clear that we want a Boolean in response, and why don't we just check to see if value, and I'm sorry, this should be an integer.
Maybe you just return true here instead, and that still returns green. So we have to be careful of trusting the test too much. Sometimes you can have all green on your test suite, but the code base doesn't work because, well, it's not doing what you thought it would. So let's add one more test here and say, well, in what condition should that return false? Well, how about something like this? Is 10 greater than 100? No, so that should return false. It's kind of a silly example, but nonetheless, it shows you the basic approach. Okay, so if I come back now, if we did that superfluous thing where we hard-coded true,
It's kind of a silly example, but nonetheless, it shows you the basic approach. Okay, so if I come back now, if we did that superfluous thing where we hard-coded true, the tests are going to inform us, uh-uh, that's not working. That's not working. There's no way that satisfies both of these expectations. So now we can write the real code, run it again, and it returns green. And yeah, as simple as this dummy little example is, it sort of represents the general flow. So if you'd like to keep working on this, yeah, on your own, just pick one of these files that we have here and write some tests for it. Start simple.
files that we have here and write some tests for it. Start simple. How about, yeah, we can force log in a User. So what you would do is you would write a test and you'd give it a descriptive name. And then you would instantiate the authenticator. You would call the login method. You would feed it a User. And then in response, you would assert that, well, there should be User in the session. So actually look into the session and confirm, is that equal to what we expect here? And if it's not, you know you did something wrong.
