Scattered Tracking Complexity0:00
Now, this next episode, I think you're really going to enjoy, even if it's the only video you watch in the entire series. We'll discuss the squint test, refactoring, use cases, and jobs. Alright, let's get going. So here is our artisan command test, and yes, it's passing, which is great, but in the process we have introduced fragments of complexity, and those fragments grow and grow and grow if you don't nip it in the bud. So let's have a chat. When we call our artisan command, in the handle method, we track every product. Okay, seems simple enough.
When we call our artisan command, in the handle method, we track every product. Okay, seems simple enough. We go to the Product model, and we track a product by delegating to all of the stock, and for each one, we tell the stock to track itself, however it needs to. And I'll hide the sidebar. Next we register what is effectively a callback. When you're done tracking that stock, we will record it to history. Okay, so very small, but this is a fragment of complexity. We are now registering something that should take place after a stock is tracked. Alright, let's keep going.
We are now registering something that should take place after a Stock is tracked. Alright, let's keep going. Into the Stock model, here's how we track a piece of stock. We begin by checking the availability through the API, so this would hit a Best Buy or Target API. Next we have some kind of conditional, it's not overly clear, but it determines whether or not to fire an event. Then we update our local stock based upon what was returned from the API, and then we trigger any registered callbacks. So now, what happens when we fire the event?
the steps associated with that are dispersed throughout the entire codebase. So now imagine you return to this project a year from now, you can barely remember it, and you're trying to figure out, okay, what exactly happens when we call track on products? Maybe you write it out here. Well, let's go through it. We're going to do it again. Well, we call track on products, but that's not really what's happening. One thing I see here is we record history. So I'll add that, record to history. Alright, that's in that file.
So I'll add that, record to history. Alright, that's in that file. Let's go back to stock. We check availability. Availability from the API. Alright, we fire an event that eventually sends an email. So notify the user. Alright, come on back. Here we update the local stock, update local stock. And then, yeah, that's it.
Here we update the local stock, update local stock. And then, yeah, that's it. Okay, so notice what three, four, five different files we had to visit to determine how and where we track stock. This is a problem. Not to mention the fact that if at some point we add a new step for how we track stock, it's not necessarily clear where that goes. And then further, all of these steps here aren't necessarily represented in the tests. We have track command, and we have one for Best Buy, and then we have product history, but again, it's dispersed through multiple files.
Introducing Use Cases3:32
We have track command, and we have one for Best Buy, and then we have product history, but again, it's dispersed through multiple files. So if you agree with me that this is a problem, let me offer you one solution that I think you might like. In my app directory, I'm going to create a directory called use cases. Any file here will represent one use case for our application, something a user can do with your application, whether it's sign up, or purchase an item, or track a piece of stock, as we're doing here. Okay, so if that's the case, let's create one called track stock. So now we have a class that represents a specific use case.
Okay, so if that's the case, let's create one called TrackStock. So now we have a class that represents a specific use case. Next, here's what I'm going to do. Let's go back to Bear, and we have those notes here. I'm just going to paste these in. We wrote out the steps for tracking a stock nicely, but again, some of these weren't represented in the code as well as I'd like. Let's fix that. I'm going to add a method here, checkStockAvailability. All right, how about checkAvailability.
I'm going to add a method here, checkStockAvailability. All right, how about checkAvailability. Next one, notifyUser, notifyUser. Next one, updateLocalStock, updateLocalStock, or let's tweak this. How about refreshStock? That's kind of what we're doing. And because we have a class called TrackStock, maybe, I'll be explicit though. And then finally, recordToHistory. All right, so now I can remove these comments, of course, because they are redundant. Now we have, in a way, containers for each of these steps.
All right, so now I can remove these comments, of course, because they are redundant. Now we have, in a way, containers for each of these steps. So the only remaining step is to trigger or call these steps, and we'll create a handle method for that. handle can be anything you want. It can be run, it can be execute, it can be perform, whatever your team likes. LayerVal often uses handle, so I will stick with that. And it sounds like we're delegating. Check availability, notify the user, refresh the stock, and record to history. All right, so here's what I want you to think about, and I'll hide the sidebar for now.
Check availability, notify the user, refresh the stock, and record to history. All right, so here's what I want you to think about, and I'll hide the sidebar for now. Ask yourself if a year from now, if you would appreciate that this class exists. With the code we had earlier, I had to visit four different files to write out these steps. However, now, a year from now, if you think, well, what happens when you track stock? How do we go about it? Well, just check out the use case, app, use case, track stock. Oh, well, we check the availability, we notify the user, we refresh the stock, and we record to history. Oh, and you need to add something new?
Migrating Logic to Use Case6:26
In my Stock model, this is where we called track, but most of this is going to leave. So I will comment this out, and yeah, if I rerun my, we'll just stick with the feature test here, of course it's failing. All right, our job is to get us to green. We're going to return to track stock, and we're going to delegate to it. Now I'm going to start by instantiating it, track stock, and then call handle. And now I'm going to incrementally migrate all of this over. So this section right here, it checks the availability. So let's throw it in that container. Now of course, as we do this, we'll run into issues, right?
So let's throw it in that container. Now of course, as we do this, we'll run into issues, right? So this, before it was assuming the Stock model, but now it's a track stock use case. So it sounds like I need to keep a reference to the Stock model. And with that in mind, we should pass it through the constructor, like so, and clean up. And no doc blocks, just to make it easier. All right, so now we pass through the Stock. Now if I come back, I can grab the Stock instance, like so. And then that returns a stockStatus. I'm not sure, maybe we return, or maybe we store that up here as well.
And then that returns a stockStatus. I'm not sure, maybe we return, or maybe we store that up here as well. We'll call it stockStatus. All right, let's keep going. Next, this section here, we fire an event, but I don't want to fire an event. We're trying to solve that problem. Events and listeners absolutely have their place, but as you saw in the last episode, when you introduce them, you also make the code a bit more cumbersome to consume. And that's what we're trying to solve. Anyways, we're going to bring this in here, notifyUser, and I'll paste it in.
And that's what we're trying to solve. Anyways, we're going to bring this in here, notify User, and I'll paste it in. Now what's going on here? It looks like we're checking if the stock is not available currently, but after we fetched from the API, it is available. Well, that means it wasn't available a few minutes ago, and now it is available, right? So in that case, we were firing an event in order to register a listener that would handle that event and then send an email. Again, notice how you're going from this file to that file to that file. It creates a bunch of misdirection, and I don't want to do that.
Again, notice how you're going from this file to that file to that file. It creates a bunch of misdirection, and I don't want to do that. So we're going to open up the listener, and I'm just going to snatch this single line, and I'm going to bring it back here, and rather than firing an event, I'm going to paste in that code directly. And then let's do this, and then update our stock instance. Now I'm going to refactor this too, but we'll get back to that in just a moment. Let's keep going. The next step is where we update our local stock database based upon what was returned from the API.
The next step is where we update our local stock database based upon what was returned from the API. So we have refreshStock, this stockUpdate, and then we'll delegate or reference the status. Finally, this callback section right here, you'll remember it's useful, but the only reason we added it was so that the Product could call each and then do something when it was done. So yet again, a micro complication. Okay, if we recordHistory there, let's see if I can just move that over. However, this is now assuming a Product model and a History relationship. So we could grab the product, or you know what, sometimes you don't always have to go
However, this is now assuming a Product model and a History relationship. So we could grab the product, or you know what, sometimes you don't always have to go through the relationship. It's just as easy to do this. Now I could say, let's see, this stock, and then we just have to include the productId as well. And that would be this stock, and I can just grab that foreign key. All right, so now you might want to keep this method. It still could be useful in its behavior that the product has. However, we were only creating it for this specific case and no other.
It's all in one line, but it's still a condition. We're saying if there's a callback, then call it. And that's a micro piece of complexity. Bring it back. And there we go. Now, reformat. And here's our stock model. Much simpler. Okay, let's see how we're doing. I think we probably still have a couple little bugs to fix.
Okay, let's see how we're doing. I think we probably still have a couple little bugs to fix. I'm going to run my test. Yeah, it fails. Track StockRetailer. Okay, where do we reference the retailer? Ah, yeah, of course. So we were assuming this was the Stock instance, but we're going to have to do that. Give it a run again. All right, history.
Give it a run again. All right, history. I forgot to import that. Run it again. And we're back at green. Okay, so all of that code that was in four different files is now nicely registered in a single use case. But we're still not done. It keeps getting better. So I'm going to close this out.
It keeps getting better. So I'm going to close this out. I no longer need to fire that event because we were only catching it in one place. Now again, if you were going to catch it in 10 different places, all right, well, you want listeners in that case. But don't reach for that until you need it. So I'm going to remove the events directory. I'm going to remove the listeners directory. I'm simplifying my code. Run it again.
Integration Testing Use Case11:46
I'm simplifying my code. Run it again. And it's still passing. All right, now, what about the testing benefits? Well, think about it. TrackStock is a thing our application does. So why don't we set up an integration test to confirm everything we've done here? And it's going to be so easy. Check this out. We're going to have a directory.
Check this out. We're going to have a directory. These are like integration tests. We'll have another one. And this will test the trackStock use case. And then real quick, because I added that new integration directory, let's just add one here. We're just adding it to our test suites. OK, and whoops, I want that here. Although use cases is fine if you prefer that.
OK, and whoops, I want that here. Although use cases is fine if you prefer that. All right, so now check this out. We go to our ClassCenterTest. And everything here, the handle method, notice that's a description of what the class does. And every step in that description is within its own method. I can now read that description and make my test. It's, yeah, I could do checkAvailability, but that's kind of a side effect. We are fetching from the API, and then these are the things that we're actually doing for the user.
We are fetching from the API, and then these are the things that we're actually doing for the user. So I almost think of that more as a private method. OK, so instead, we'll say it notifies the user, it refreshes the local stock, and then it records to history. So notice these test names were so easy to write because they map to the use case. Now these tests were in some form represented in some of these other files. However, we're going to reproduce them really quick to show you how easy it is. If it notifies a user, well, let's fake notification, and then ultimately assert. Let's just keep it simple.
If it notifies a user, well, let's fake notification, and then ultimately assert. Let's just keep it simple. Assert that one notification was sent, and it is important StockUpdate. All right, so this is our final assertion. To put things into motion, well, let's just instantiate our TrackStock. New TrackStock, that'll need some kind of stock, and then we call handle. So with that in mind, let's seed our database like we've done many times, and that'll give us a Product and a Stock and a User. Then I can just grab the first stock from that seed. All right, seed our database, then call the handle method.
Then I can just grab the first stock from that seed. All right, seed our database, then call the handle method. That'll run through all of the necessary steps, and then if we did everything correctly, among those steps, we should notify the User. Now are we all set to go? Why is this an error? Not found. Oh, yeah. We want to make sure we import Layer or ElseTestCase, and then of course we're going to hit the database.
So now we've asserted that for this use case, we notify the User. Okay, let's do another one, but notice I'm going to have to copy a lot of this. So we're going to do it one time, and then extract to clean up the code. It refreshes the local stock. So this time, I'm not interested in reviewing the notifications, I just want to make sure that the stock database is updated. All right, so this assertEquals, and if we take a look at this real quick, here's what we mock the API, and it's going to return that the stock is available, and it's $299. Let's make it a little more clear, though. Available is true, and price will be $249.
Let's make it a little more clear, though. available is true, and price will be $249. So notice I'm using these temporary variables. Another option, if you are a phpStorm user, is to turn on parameter hints, just search parameter hints. I have that set to a keystroke, and you'll see it adds a little overlay hint there that you might like. Anyways, I don't often do it for screencasts, because I think it might confuse people who aren't familiar with phpStorm. Okay, so anyways, assertEquals $249, and we got to grab the stock.
aren't familiar with phpStorm. Okay, so anyways, assertEquals $249, and we got to grab the stock. Let's do this. Let's grab the stock. Remember, we only have one piece of stock, so I can be pretty loose about fetching it from the database. And then we'll say stockPrice. So we give it a run, and it passes. But of course, if we never called that use case, it would fail. Okay, our final assertion is assertThat it is available.
But of course, if we never called that usecase, it would fail. Okay, our final assertion is assert that it is available. So stock, what is it, in stock, and that should work too. Okay, so now let's remove some duplication, and I'm going to override the setUp method. Okay, so our setUp, we're going to say we're never actually going to fire off a notification, so we can always fake that. Then we're going to mock the clientRequest, and it doesn't necessarily matter, so I'll just grab that one there. Then we want to seed the database. All right, and I think that is our setUp.
Then we want to seed the database. All right, and I think that is our setup. Let's see. Then I get rid of that, and that, and let's see, does that still work? Yep. And yep. Next, it looks like we call the handle method in the exact same way every time. So could we just do that? And then each of our tests become far more simple. Does that work?
And then after we track the stock, make sure that it updates our local database. One more time. After we track the stock, it records to history. So I could say this assertEquals one, and if we just want to be quick, read the number of records in the history table, and that should be one, and that returns green as well. Or fetch a relationship on the product, whatever you want. I'm just trying to be quick here. But yeah, it ends up being so simple if we take this approach. So now, if we have a dedicated place where I assert that we notify the user, yeah, then you can clean up your other files.
So now, if we have a dedicated place where I assert that we notify the User, yeah, then you can clean up your other files. Like on my FeatureTest, this can return to being an entry point for the php artisan command. It doesn't need to test whether or not notifications are sent. So these can safely be removed, and that cleans up the code a little bit. Reformat. Now this is passing, and of course our IntegrationTest is passing as well. Pretty cool. So one final piece of the puzzle. If we come back up, we now have a directory for use cases that describe a use case for
So one final piece of the puzzle. If we come back up, we now have a directory for use cases that describe a use case for the application. And the contract, you could be formal, by the way, and actually enforce that, is simply that the use case has a handle method. The handle method declares the steps, and then each step is contained within its own method. Oh, and by the way, real quick. This here can be extracted to a method so that we don't always have to parse what it means.
This here can be extracted to a method so that we don't always have to parse what it means. We could just say, is now in stock, or something like that, and that'll still pass. Yeah. All right. Bring that down. There you go. All right. So now, Product model is looking pretty sharp. Stock model is looking much sharper.
Use Cases vs Jobs19:28
So now, Product model is looking pretty sharp. Stock model is looking much sharper. And now if we ever want to see how we track stock, we have a dedicated place. Okay. So what about if we're tracking 100 pieces of stock? There's a lot going on here. So ideally, I'd want to throw this job on the queue. And when I say that, you might start thinking, well, I know Laravel has job classes. So when would I use a use case, and when would I use a job? And the answer is, well, they can be the same thing.
So when would I use a use case, and when would I use a job? And the answer is, well, they can be the same thing. Have a look. If I were to make a Job, trackStock, let's see what it looks like. It'll create a jobs directory, but there's nothing special about that. It has a bunch of imports, but most of these are little helpers. Dispatchable simply adds some static methods on the class to make for a cleaner API. So you could do things like trackStock, dispatch. That's all this file does. What else?
That's all this file does. What else? Of course, you can implement shouldQueue to signal to Laravel that it should be queued. Shocker. is if you need to interact with the queue, and often you don't. So notice it has a handle method, nothing overly special here. So we could treat a job just like a use case, and you have two options. One, think of them as synonymous. So if you want to create a use case, just throw it in the jobs directory, and you're good to go.
So if you want to create a use case, just throw it in the jobs directory, and you're good to go. Or, if you like the term use case, well, stick with it. We come back here. So we're already adopting the correct method API. So if I come back here, why don't we just dispatch a new trackStock? And now just as a sanity check, if we didn't do anything there, our feature test is going to fail. However, if I bring it back, we're treating it like a job, and now it returns green. So we were creating a use case, but it's effectively a job.
However, if I bring it back, we're treating it like a job, and now it returns green. So we were creating a use case, but it's effectively a job. Which further means, if you want to use that dispatchable trait, you could say trackStock dispatch. Now it's going to fail, right? You'll see that squiggly there. And that's because, well, we need to use that trait. Use dispatchable. Run it again, and now it works. So the same thing here.
This is just a simple explanation with no code.