Why Tests Enable Refactoring0:00
Let's begin with technique number one. If you'd like to write cleaner code, there's no easy way to get there without first having a series of tests backing up every change you make. Because think about it, without those tests, it's simply too risky. Why take a chance improving a bit of code when there's the opportunity to break everything? You know? With that in mind, most of us simply tuck the code away. We know it works, but we don't really want to touch it because, again, it's simply too risky to make a change. All right, so let's review an example here.
Reviewing the Artisan Command0:32
risky to make a change. All right, so let's review an example here. I have a Laravel Artisan class called SendUpgradeCouponToMonthlySubscribers. And here's how you trigger it, php artisan, laracast, blah, blah, blah. Okay, now if we come down to the handle method, what I like about this example is it's simple but real-world enough to the point that we can very quickly parse it, we can add a test, and then we can discuss any potential changes we should make to it. And in fact, in this case, it's somewhat procedural, but you know what? It's really easy. So any changes we do make would be minimal.
It's really easy. So any changes we do make would be minimal. Okay, let's quickly go over it. Find me all active users on the monthly plan who signed up exactly three months ago. Once you have that collection, iterate over them, generate a new coupon for 10% off exactly one time, and then send them an email. That essentially says, and in this case it's a mailable class, that essentially says, hey, if you upgrade to the yearly account, here's a 10% coupon to lower the price. You know, a standard marketing email that many similar businesses will use. Okay, so let's switch back.
Writing the Initial Test1:33
You know, a standard marketing email that many similar businesses will use. Okay, so let's switch back. Now that you understand how it works, let's fill in a test. Now I've already created a test class behind the scenes, so we can begin by simply documenting what the php artisan command does. How about it sends an upgrade coupon to new monthly subscribers? And this is a side effect to adding a test. Not only does it ensure that the code works, but it also serves as documentation for the class. Well, given we have a User who signed up three months ago, when I run this artisan command, then they should receive an email.
Well, given we have a User who signed up three months ago, when I run this artisan command, then they should receive an email. Oh, and also, we should generate a coupon for 10% off. All right, this is the basic path. All right, let's get started. I'm going to begin with a factory and my namespace here at Solaris. Okay, given I have a User, however, that User needs to have signed up exactly three months ago. So I can use this today helper that will return to me a Carbon instance for the beginning of today. And then I can sub exactly three months. Okay, next, when I run the artisan command.
And then I can sub exactly three months. Okay, next, when I run the artisan command. All right, well, I can say this artisan. And now if we switch back, you'll see this is the signature. It's how we call it from the command line. So I can switch back and paste that in. We should generate a coupon. So I'm going to hold off on that for just one second. And then we'll talk about how to deal with it because it's a little tricky when you're dealing with third-party APIs.
And then we'll talk about how to deal with it because it's a little tricky when you're dealing with third-party APIs. Okay, next, they should receive an email. Well, we can do that pretty easily. I can say on our Mail facade, I don't actually want to send email. We're going to fake it. And instead, that's going to return to us a MailFake instance where we can call various assertions like assertSent, assertNothingSent, assertSentTimes, and things like that. Okay, let's switch back. And we'll say Mail::assertSent.
Okay, let's switch back. And we'll say mail assertSent. And I already have a Mailable class called UpgradeToYearly. And well, actually, we'll come back to that in a moment. This is our basic test. And if I give it a run, it passes. However, notice it took a little bit of time. That took almost two seconds to run, which is way too long. Let's give it another run. Yeah, a little bit quicker, but still, almost a second for a single test is not what you want,
Avoiding External API Calls4:06
Let's give it another run. Yeah, a little bit quicker, but still, almost a second for a single test is not what you want, unless you are writing a test that should hit an API and you depend on that. But that should be a small portion of the overall test you write. So this is related to the fact that we are, right here, generating a Coupon. And behind the scenes, that's going to hit, in my case, Stripe's API. It will create a Coupon on their servers. So for example, if I were to simply here, like, new up a Coupon class, but not do anything. And if we run the test again, you'll see this time it's very, very fast. Okay, so it sounds like we need to intercept it, because I'm not testing.
And if we run the test again, you'll see this time it's very, very fast. Okay, so it sounds like we need to intercept it, because I'm not testing that the call to Stripe's API works. I have other tests that will confirm that. So in our case, it sounds like we just want to say, all right, well, I want you to send the command to create the coupon, but I don't actually want you to make a curl request to Stripe. And you'll see the way we're doing that is this handle method is resolving what I call a gateway class here. It's like a StripeGateway.
what I call a Gateway class here. It's like a Stripe gateway. And we are passing that to the couponGenerate method. And this is one technique I like to use, because it allows you to stick with a nice readable API. However, for any calls that should delegate elsewhere, we'll just pass that in as an argument. That way, I'm not using a bunch of StripeGateway sender, you pass in the coupon, and it all gets kind of confusing. What I'm really doing here is generating a coupon. So I want it as readable as that. Okay, so it sounds like we need to mock the Gateway class.
Mocking the Gateway Dependency5:35
So I want it as readable as that. Okay, so it sounds like we need to mock the Gateway class. Let's do that now. Now, in later versions of the Laravel framework, I haven't upgraded the Laracasts codebase just yet. But there is a mock method on your test classes that will mock a class and then substitute it within the container. And I do recommend that. But for now, I don't have that. So instead, I'll just do it directly.
But for now, I don't have that. So instead, I'll just do it directly. I'm going to call mockery. And we're going to spy on that Gateway class. And I'll call it our gatewaySpy. Next, we're going to swap the instance of this Gateway class in the container, we'll talk about this in a minute, with our spy. Now, if you're not familiar with the swap method, all it does is it swaps out the current instance,
Now, if you're not familiar with the swap method, all it does is it swaps out the current instance, the binding in Laravel service container with something else. So now what we've done here is we've told Laravel, well, if you ever need to resolve gateway out of the service container, I don't want you to instantiate the Gateway class directly. Instead, I want to send through this. So take a look. If I switch back, let's just dump or die and dump the gateway. And I'll show you if we run the test,
If I switch back, let's just dump or dd the gateway. And I'll show you if we run the test, you'll see what we get the original StripeGateway class. You know what? That's because we mock it and we swap it out in the container after we call php artisan. That should happen before, of course. So anyways, yeah, if we don't swap it out and we run the test, we get an instance of StripeGateway resolved. But once we swap out that instance in the container, now we're going to get a mocked version, as you see there.
But once we swap out that instance in the container, now we're going to get a mocked version, as you see there. OK, so now this is what we want, because I could say we should generate a coupon right here. I could say, well, a gateway should have received a call to a method called createCoupon. And in this case, I don't care what argument it gives. So I'll just say anything, and that should be done exactly once. OK, so if I come back, let's remove the die and dump. And if we give this a run, everything still works. We get green.
And if we give this a run, everything still works. We get green. So take a look. If we scroll down here, if I were to not generate this coupon, and instead, like we did before, we just instantiate the coupon, now it's going to fail. We expected you to call createCoupon one time, but you didn't do it at all. So we have two benefits here. We have asserted that this command is being sent, and we've also blocked any unnecessary API calls that were being made to Stripe, which improves the speed of the test.
and we've also blocked any unnecessary API calls that were being made to Stripe, which improves the speed of the test. If you're curious why we're asserting createCoupon, that's because behind the scenes, when you generate a coupon, this method I'm not going to show it to you because a few things happen there. But when you call it, it's actually going to call gateway->createCoupon. And that method will delegate the API call to Stripe. So all we're doing here is saying, OK, just mock it. I don't actually want to trigger any of those methods. And then in the service container, swap out the gateway instance with our mock.
I don't actually want to trigger any of those methods. And then in the service container, swap out the gateway instance with our mock. That way, we can say, well, I do expect that you sent this command to the collaborator. And yeah, in general, as a basic tip with mocks, I would say be very careful because the more you use mocks, there's absolutely a benefit. But also, you get a little more coupling there because now I can see in order to test this class, I have to call the createCoupon method. But what if I wanted to create the coupon in a different way? Well, even if I was doing it correctly, this test would still fail.
However, one quick thing. Let's make sure we send an email exactly one time. There we go. And in fact, we could even pass a closure here if we wanted to be absolutely sure that it was being sent to this user up here. And there's our test. OK, but now there's one last thing. There's an issue here that the test is not picking up. Often, I will like to create a control. So yes, I expect a User who created their account three months ago to receive an email.
Adding a Control to Find Bugs10:05
Often, I will like to create a controller. So yes, I expect a User who created their account three months ago to receive an email. Let's do one more. Let's create another User from a month ago. With this change, I do want to assert this guy received an email. This one did not. OK, now if I give it a run, uh-oh, it fails. We expected a call to create a coupon one time, but it happened two times. And this is not right. We should only be contacting the User from three months ago.
And this is not right. We should only be contacting the User from three months ago. So we've spotted a bug by adding a control of sorts. OK, let's fix that. And in fact, we don't need the user variable there. But I might say, this person should receive an upgrade coupon, this person should not. And whether or not you keep these comments only for yourself while you're working out the test, or if you keep them permanently, entirely up to you. Sometimes I'll do a bit of both. Like here, I don't get much use out of it because it's repeating what the code does.
Sometimes I'll do a bit of both. Like here, I don't get much use out of it because it's repeating what the code does. And the same thing here. We should generate a coupon. Gateway should have received a call to create a coupon. You know, it's basically the same. So I might keep this. OK, once again, I run the test. It fails. I need to fix this.
It fails. I need to fix this. And the issue is right here, a bug I introduced at the beginning. So we would spot this. Find me active users on the monthly plan where the day is three months ago. Where the day is, not the date. So let's tweak this. We want it to be where the date is three months ago. And now with that change, if we run it and we get green, we have fixed the bug. And you might even want to do other things.
If you don't trust your tests, then you're not going to run them as often. And you're not going to refactor because you think, oh, sometimes the test is passing, but I know the code's not working. You just don't trust yourself. You need to make sure you trust your tests. So again, if I run this, it returns green. But yeah, it also ensures that if by any chance you change a bit of code, it's going to pick up on that. It's going to let you know, hey, something's going on here because we're calling this createCoupon method more than we should.
It's going to let you know, hey, something's going on here because we're calling this createCoupon method more than we should. Or even if we didn't have that, if we run that, it will say, hey, I sent two emails to upgrade, but you only expected me to do it once. So something's going on here, right? That's the sort of assurance we're given here. All right, so I'm going to bring that back. And now that I have a set of tests backing up this class, in the next episode, we'll review technique number two.
