PHPUnit Tutorial Part 2 - Mocking - Full PHP 8 Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] in the previous lesson you learned how to write unit tests using php unit we wrote unit tests for a very simple basic router class that had no dependencies but what if we need to write a unit test for a class or a method that has dependencies that is what we'll be covering in this lesson you will learn about test doubles stubs and mocking as you know in unit tests we try not to resolve dependencies instead we try to replace the original classes with the test doubles or fake objects which can be done by mocking mocking simply allows us to fake dependencies of the method or a class that is being tested and swap the real class dependencies with the fake ones there are many examples where mocking can be really useful like mocking database related classes models email and sms sending services api calls and so on you wouldn't want to send out real sms or email every time you run the test right and because apis can't be guaranteed to have hundred percent uptime your tests would sometimes fail or sometimes pass depending when the api is reachable because api can be unreachable or down for maintenance and so on also it could take a while to make api calls within your test because of the latency therefore increasing the time that it takes for your tests to run let's take a look at this example i have the invoice service class that has a process method which processes the invoice we're passing some customer info here along with the amount and note that this is a very simple example and in a real application you would use objects instead of arrays in a place of customer info for example but i just wanted to keep this very simple now in a typical invoice processing scenario and again this will depend a lot on the specific application you would at least need to calculate sales tax which can be done by making maybe an api call to a sell stock service that handles that for you and then you would need to process the invoice using the payment gateway which could be stripe paddle or any other gateway and again it would connect to the api to do that and then after the payment has been processed you would send out a receipt to the customer if payment was successful and if it was not successful maybe you would send another type of email or handle it differently and this is the example that we're trying to simulate here as you can see we have the sales tax service dependency we have the gateway service dependency and we have the email service dependency as the first step we're calculating the sales tax here passing the amount and customer then we're processing the invoice using the gateway service passing the customer amount and tax and then we're sending the receipt email if the processing was successful because if it was not successful it would return false and then once everything is done we're returning true now when writing an unit test for this we don't want to use the real api calls to calculate the sales tax or process the invoice and we definitely don't want to send out a real email right this is where mocking can help we can fake these dependencies and avoid making real api calls and sending real emails so let's write some unit tests we need to create a new directory called services under the unit directory and within there we need to add a new test class called invoice service test let's close this out add the strict types and extend the base test case class i'm also going to open the invoice service class in a split view so that we can see what we're writing the tests for we want to test that an invoice is processed successfully and we don't care if email was sent or not or sales tax was calculated or not we just care that invoice was processed successfully so let's write the test method public function it processes invoice and if you look at the invoice service class here in order to test that invoice is processed successfully the process method should return true and for that to work the charge method from the payment gateway service class should also return true so let's set up the test so given the invoice service class or given an object of invoice service class when the process method is called then we want to assert that invoice was processed successfully so let's fill this in now given the invoice service class and when we call the process method passing some kind of customer and amount arguments then we want to assert that invoice was processed successfully so we need to save this into a variable so we'll call it result and then we need to assert that the result is true and for that we can use assert true method now in this case it doesn't really matter what the customer or amount variables are so we'll just set it up to something very simple i'm going to set this to something like name equals geo and the amount will set it to something like 150. let's run this test and see if it passes and i'm going to run the tests just for the invoice service test class and we see that it failed and it also took a couple of seconds right let's run it again we see that it failed again let's run it one more time and we see that this time it passed but it also took three seconds so every time we run it takes about three seconds but sometimes it passes and sometimes it fails so let's understand why this is happening and what's going on this is actually expected because i set up the dependent classes in a way to simulate api calls by adding some delay so let's open dependency classes here so i'm going to open the calculate method on the sales tax service and we see that we have the sleep function here that sleeps for one second then let's open the send method on the email service and we see that this also sleeps for one second and let's open the charge method on the gateway service and this one also slips for one second so it makes sense why it takes about three seconds to run the tests the charge method also returns true or false randomly so that's why it sometimes passes and sometimes fails and this is there to simulate that api calls will not always guarantee 100 successful calls so the real problem is that when we run the test it makes the method calls on the real original objects and that's not ideal because we don't want to be sending emails so the solution for this is to fake the dependencies and replace them with test doubles we can create a test double by using a create mark method so let's create marks of all the three dependencies so we need to create marks somewhere here so we'll do sales tax service mac equals this create mock and we'll pass the full qualified class name sales tax service class let me make this a little bit bigger so it's easier to read and we'll duplicate this two times and we'll simply fill it in for gateway and email service classes so now we have what is called test doubles we have fake objects of sales tax service payment gateway service and email service we can actually call the methods on these objects we can do sales tax service mock calculate and pass some arguments like amount and some customer in this case it doesn't really matter i can pass in an empty array just to demonstrate all methods by default of the marked objects return no so let's var dump the result of this so we can see what we get i'm going to return from here so that it does not continue executing the rest of the code let's open the terminal clear this out around the test and if we scroll up we see that it's returning zero with the data type of float now this is not formatted properly so maybe instead of return i'm gonna add exit and let's run it again so we see that it's returning zero with the flow data type but i said that all methods by default return no the reason it's returning flow data type is because we have it type hinted on the calculate method we have a type hint here float and therefore it's casting the null into float and that's why it's returning 0. if i remove the type hint and run the test again we see that now it's returning no so this is just to show you that all methods by default will return null on the marked objects unless you have a type hint in which case you will try to convert it to that type so let's add the type hint back and let's get rid of this here and let's see if this works this should technically work right let's switch back to the invoice service here so we are faking the sales tax service payment gateway service and email service classes so you should not call the original methods you should call the method on the marked objects right so let's run the test to find out i'm going to run the test and we see that it's waiting a few seconds again and the test fails let's run it again and we see that now it passes so as you can see it is still executing the methods on the original class and not the methods on the faked class so the problem here is that dependencies are hardcoded directly in the method right here and when we're creating the fake objects of these classes within our test it is not going to magically replace all the instances of this class with the fake classes so instead of hard coding dependencies like this and then hacking it away to somehow mark them with the fake ones we can use dependency injection to make testing easier and then we can simply pass the test doubles or mocked objects in the places where the original object is expected so let's accept these dependencies as arguments in the constructor so i'm going to create a constructor here and i'm going to make this a bit bigger for now and let's add these dependencies within the constructor arguments so we need protected sales tax service sales tax service and i'm going to put them on its own lines so it's easier to read i'm going to duplicate this two times and we're simply going to replace this with payment gateway service and email service now we can simply get rid of these from here and use this variable when trying to access these dependencies so this is step one now we have removed the hard-coded dependencies from the process method this process method no longer cares how the sales tax service is created it simply knows that sales tax service is given to it and it calls the calculate method on it so when we're writing the test right here when we're creating the invoice service class object we can pass the fake classes here instead of using the real classes so instead of doing something like new sales tax service here we can simply pass sales tax service mock and gateway service mock i should have named this mock here and same here and we're going to pass email service map so let's run the test now and see if this helped as you can see the test is still failing but it didn't take three seconds it was instant right if we scroll up here we look at the time we see that it didn't take anywhere near three seconds like it did before so this is a good sign because we know that methods from the original classes did not run and the methods from the faked classes ran instead and as i mentioned before by default all methods from the faked classes return null or whatever the type hint is and in this case the type hint for the charge method on the gateway service class is boolean and therefore when null is cast into boolean it's returning false and therefore our assertion fails to fix this we need to stop our marked object to return true when the charge method is called on the payment gateway service class we can do that by stopping the method return for the charge method on the payment gateway service class so we can go here and do gateway service mock method charge will return true now let's run the test and see what we get let's clear this out run the test and sure enough it passes so what we're doing here is that we're saying that we want to test that invoice can be processed and we're creating fake objects of the dependencies that the process method needs and we don't care about their methods except for the charge method because that needs to return true in order for our process method to work so we are stubbing the charge method on the faked gateway service class and are specifying that this method should return true no matter what if this sounds a bit confusing to you don't worry about it it will make more sense as you practice and write more tests there is one more test i want to write here yes we tested that invoice can be processed successfully but we haven't tested if the email was sent successfully so let's create a test for that i'm going to add a test method here public function it sends receipt email when invoice is processed and in here we're going to copy this section here because we're going to be mocking the dependencies so we'll put it here and right away you can see that we could extract this section within the setup method to avoid the code duplication but how do we assert that email was actually sent we need to somehow check that email was sent which means that we need to assert that this send method was actually called this is where mocking really comes in handy we can set up expectations on the marked object so we can say that email service class is expected to call the send method with the given arguments so for example we can do something like this email service mark expects and we'll say that this expects the method to be called one time so we'll do this once then we'll say that it will call one time the method send and it will call that method with the given arguments and we'll say that the arguments will be named geo that is the first argument which is the customer and the second argument is in this case receipt because that is what is being passed here you can think of it as a template or something like that it doesn't really matter and of course this doesn't have to be hard-coded here it could be extracted into something else but just keeping things simple here so let's format this so it's a bit more readable i'm going to put these on its own lines so to explain this better what we're doing here is that we're expecting the send method to be called one time and the arguments passed to the send method should match the arguments that we've specified within the process method and we haven't written the process method yet so in here we need to copy essentially this part right here and we don't really care about this assertion here but we're going to leave it in because we want to make sure that email was sent and invoice was processed successfully now let's run the test and see if that passes i'm going to run the tests as you can see two tests and three assertions were detected and they all passed if we change this argument to something else for example something like john and run the test we see that it fails and the error message says that expectation failed for the method name send when it was invoked one time and the parameter did not match the expected value now you might be asking this is kind of pointless right why are we hardcoding here can't we simply just do customer and move this customer definition from here to the top and then we will make sure that it will always pass and the answer is yes we can do that but what this actually is testing is not the validity of the customer array within our test but it's testing whether or not we're passing the correct arguments to the send method when it's called within the process method because if later while refactoring we made changes to the structure of the customer array where it would maybe contain additional data like an id or something the test would fail let's run the test and sure enough as you can see now it fails it would also fail if we change template to something other than receipt and so on so we're basically setting an expectation that the send method has to be called with the given arguments and if the arguments do not match then the test should fail so that we can find out why the arguments didn't match and either adjust the test or fix the code so i think this wraps up the intro to php unit and testing this should be good enough to get you started with testing you will get familiar with the rest of the stuff like how to mock and test different things along the way as you're writing tests and when the needs arise one of the main benefits with writing tests in addition to finding bugs and automated testing is that it pushes you to write better code the way i look at it is that if my test is becoming too complicated where i need to mock a lot of things or things don't make sense then i try to see if i can maybe refactor the method itself so instead of making test work for a complex method i try to simplify the method first and then write a simple test for it we made one such refactor in this lesson already we were using hard-coded dependencies and moved to using dependency injection instead which made writing unit tests for it easier and we'll talk more about dependency injection in a separate video so this is it for this video thank you so much for watching if you're new to the channel consider subscribing and if you liked the lesson and found it useful please smash the like button so that more developers can watch engaging with my videos by liking and posting comments really helps my channel grow so thanks again and i'll see you next time
Info
Channel: Program With Gio
Views: 44
Rating: 5 out of 5
Keywords: php, php8, php tutorial, php course, learn php the right way, object oriented php, full php course, php in 2021, advanced php course, php tutorial for beginners full, php oop tutorial for beginners full, phpunit, how to fake dependencies in php, how to mock classes in php
Id: EhkeoV8nfCQ
Channel Id: undefined
Length: 16min 20sec (980 seconds)
Published: Tue Sep 21 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.