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

Testing Modules in Isolation0:00

Hey there, welcome back to a new lesson. Today we are going to talk about how to test modules in isolation. For example, within the Product module, we have a class called PurchaseItems. And the PurchaseItems class communicates with the class that lives in the Payment module, which is responsible for charging a credit card. Now, within your task, it's possible that you want to test the PurchaseItems class in isolation. You don't want to have any outside actor interfering with it. For example, the class that lives within the Payment module, you want to test that separately. And within the PurchaseItems class, you just want to make sure that whatever that class is responsible for works. Of course, this is an option and it has its pros and cons.

And within the PurchaseItems class, you just want to make sure that whatever that class is responsible for works. Of course, this is an option and it has its pros and cons. You can have the PurchaseItems class just test everything, including external actors. But that's not what this lesson is about. In this lesson, we're simply going to see how to replace those third-party classes with something that does not interfere with our tasks. So let's jump into the code. Alright, so this is the class that we are testing. It's the PurchaseItems class. And as you can see, it interacts with the CreatePaymentForOrder class. It also interacts with the ProductStockManager, but that's within the same boundary.

And as you can see, it interacts with the CreatePaymentForOrder class. It also interacts with the ProductStockManager, but that's within the same boundary. It's also something that's owned by the Product module. CreatePaymentForOrder, however, lives in the Payment module. And the train of thought here is you don't really care how the class is implemented. It belongs to another module. You're just calling it. So it's not the responsibility of the PurchaseItems task to test the CreatePaymentForOrder. Now, in reality, in practice, that's not how things are often done. Sometimes it makes sense for you to just have an integration task that touches everything, including external actors.

Now, in reality, in practice, that's not how things are often done. Sometimes it makes sense for you to just have an integration task that touches everything, including external actors. That is, however, up to you. There's no silver bullet. You have pros and cons. But if you want to test something in isolation, this lesson will help you. This class is pretty simple. It creates an Order. It creates a Payment. And then it returns a DTO.

Reviewing Test Cases1:56

It creates a payment. And then it returns a DTO. It also dispatches an event. Most of the logic is within a transaction. So if something fails, no order is going to be created. If the payment fails, the user is not going to have an order without having paid. Let's take a look at the task. Our road to test cases. The first one is the happy path. So we're setting up the stage for our test.

The first one is the happy path. So we're setting up the stage for our test. We're creating a CartItemCollection, a PayedInPayment, and also a UserInAproduct. Then we are instantiating the PurchaseItems class. We're calling the handle method, passing the CartItemCollection, the PayedInPayment, and the User. And then we're just making a bunch of assertions. If the order has the correct total, if the order has the correct total of lines, the correct product ID, the correct product price and cents, and the correct quantity. We also make sure that we dispatched an OrderFulfilled event that belongs to the correct order and to the correct user. Now, what we want to replace is the CreatePaymentForOrder class, which is passed in the constructor. And we are using Laravel to instantiate the PurchaseItems class.

Now, what we want to replace is the CreatePaymentForOrder class, which is passed in the constructor. And we are using Laravel to instantiate the PurchaseItems class. Before we go any further, let's take a look at another test case I wrote. This is the unhappy path. What happens if the payment fails? Well, for example, we need to expect an exception, since we do expect the exception to flow through the method. And we also expect no orders to have been made, no payments to have been made, and no OrderFulfilled to have been dispatched. Now, the first way that you can replace a class is with mocks, which is what I did here. We have a mock being created and replaced within the Laravel container, so that when we instantiate this class, what it receives is not actually the real CreatePaymentForOrder class, but a mock. And then we are telling the mock to allow the handle method and to throw a new paymentFailedException.

Replacing Mocks with Fakes3:29

We have a mock being created and replaced within the Laravel container, so that when we instantiate this class, what it receives is not actually the real CreatePaymentForOrder class, but a mock. And then we are telling the mock to allow the handle method and to throw a new PaymentFailedException. Mocks work well for this, but they have their disadvantages. First, they are really hard to refactor, and you can introduce some undefined behavior with them. Instead, I want to show the concept of a fake object, just like we have with the payment provider, with the payment gateway, and instead of using mocks, simply replacing the real object with a fake one. So let's start with the happy path test. Let's run it. Both tests are passing, so we're good to go. If you're using fake objects, the first thing that you're going to need is an interface. Since now you're going to have multiple implementations of that, at least two, the real one and the fake one. So let's go within our payment module.

Introducing an Interface4:20

Since now you're going to have multiple implementations of that, at least two, the real one and the fake one. So let's go within our payment module. We have our action, and now you have two options. Some people like to call the interface just the actual name, CreatePaymentForOrder. Some people like to add the interface suffix. In our case, just so we don't mess with what you have already written, let's create a new one. So let's create a class, an interface really, called CreatePaymentForOrderInterface. Let's copy the signature and add it to our interface. Let's import the classes, and there we go. So we can now go back to our real implementation and implement that interface.

Let's import the classes, and there we go. So we can now go back to our real implementation and implement that interface. Within PurchaseItems, we can replace the stipend for CreatePaymentForOrderInterface. Now, if we run a task, it's going to fail. And that's because Laravel does not know how to resolve an interface, so we also have to tell it to bind this interface to our real implementation. Let's go into our PaymentServiceProvider, and just like we did with the payment gateway, let's bind the interface to our real implementation. Let's add a semicolon, there we go. Now let's rerun our tasks.

Let's add a semicolon, there we go. Now let's rerun our tasks. Okay, we have one task failing, and that's because within our second task, we are type hinting the actual implementation, not the interface, so let's update this and rerun our tasks. Okay, now they're both passing. Now we can finally create a second implementation. Let's go back to our actions folder and let's create a file called, for example, CreatePaymentForOrderInMemory. We have to implement the CreatePaymentForOrderInterface.

Building an In-Memory Fake5:58

CreatePaymentForOrderInMemory. We have to implement the CreatePaymentForOrderInterface. Let's add the method stub, and now we don't really want to create a payment, so let's just store that in memory. Let's create an array, let's make it public, which is going to be initialized. As an empty array, let's instantiate a payment and just fill it with data. There we go. Now we can push that within our payments array, and then we can return the payment. Perfect. Now let's go back to our purchaseItems task, and let's start with the happy path task.

Perfect. Now let's go back to our purchase items task, and let's start with the happy path task. The first thing we want to do is to instantiate the fake object, so we can say that CreatePayment is going to be the CreatePaymentForOrderInMemory, and after that, we're going to bind that into the container. Let's say that the CreatePaymentForOrderInterface should now return an instance of CreatePayment. This instance is specifically this one, and let's rerun our tasks. They're passing. If after we run the action, after we execute it, we dump in the CreatePayment, we're going to see something interesting.

If after we run the action, after we execute it, we dump in the CreatePayment, we're going to see something interesting. We have the CreatePaymentForOrderInMemory, and we have the array of payments. So now we can also add assertions based on the fake object that we have. For example, we can say that we should only have one payment, so let's do an assertCount. We can fetch the payment by capturing the first index of the array and do assertions on top of it as well. For example, we can make sure that the payment has the right userId. Let's run this, and it's passing.

For example, we can make sure that the payment has the right userID. Let's run this, and it's passing. If I were to type some gibberish here, it's failing. And let's go ahead into our fake and also add a dot block here saying that this is an array of payments. So there we go. And now we should have proper type hinting. So if I were to write you, it knows we're talking about a Payment instance. And if you want, you can add helpers like if it has assertDispatched into your fake object as well.

And if you want, you can add helpers like if it has assertDispatched into your fake object as well. Now we can do the same thing here. Instead of using a mock, we can also use a fake object. Let's copy this, replace this with the fake object. And for example, we can instruct it to fail. Let's add a shouldFail method. We have to add the method into the class. Let's just set a flag saying that it should fail. It shouldn't return anything.

Let's just set a flag saying that it should fail. It shouldn't return anything. Let's initialize this property as well. And then with this method, we can simply check whether it should fail. And if it should fail, then we just throw an exception. Let's go back here and test this. And it's also passing. So this is failing as it should. And if you want to, you can also make assertions here. Let's say that we want the count to be zero.

And if you want to, you can also make assertions here. Let's say that we want the count to be zero and pass the number of payments. Well, the array of payments. Let's run this, and it's passing. As you can see, it's pretty simple. The benefit of this is since you have a common contract that both implementations follow, both the fake one as well as the real one, you have assurances that if you were using the real implementation,

Isolation Pros and Cons9:06

both the fake one as well as the real one, you have assurances that if you were using the real implementation, it would work, as in you wouldn't have an argument error, for example. The signature would be correct. Now, would it actually work? Would it be implemented correctly? Well, that's something that you can test in isolation within the task for the CreatePaymentForOrder class. With that said, whether you want to run your tasks in isolation from other modules is up to you.

With that said, whether you want to run your tasks in isolation from other modules is up to you. As usual, there are pros and cons. The pros are that you're now isolated from the module, and what happens there won't affect your module, your task. If they decide to change the internal implementation of the method or change how a payment is represented, you wouldn't have to worry about that. The disadvantage is whenever you run tasks in isolation, it becomes easier to miss things.

The disadvantage is whenever you run tasks in isolation, it becomes easier to miss things. I'm not saying that's necessarily true, but it becomes easier for you to think you're testing something when you're really not. For example, if both implementations do not follow the same contract, you might have unexpected behavior. Or if you did not write tasks for the real implementation in isolation, you're not sure if it would work. Or if you have edge cases within the class that you're testing.

you're not sure if it would work. Or if you have edge cases within the class that you're testing. So you really have to pay attention for this. Don't do something because someone told you to, even if that's someone's speed. Do things if they make sense to you. If you think that testing something in isolation does not make a lot of sense, it probably doesn't. Each project has its differences, and again, there's no silver bullet. So take everything that I say,

Each project has its differences, and again, there's no silver bullet. So take everything that I say, take everything within this course with a lot of caution, and do what makes sense to you. Sometimes you're wrong, and you're going to have to fix it later. But it's important to trust yourself. With that said, that's pretty much all I have for this lesson, and I'll see you in the next one. Bye-bye.

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