PHP Unit Testing - PHPUnit Tutorial - Full PHP 8 Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] in the last lesson we talked about different methods and techniques of testing in this lesson we'll actually write some unit tests and learn the basics of php unit which is a testing framework for php first we need to install the php unit so let's go to the php units documentation and follow the installation steps there within the installation section we see the option to install it with composer so let's click on that and we'll copy the command and paste it into the terminal so this is the command that we're going to copy and notice that we're installing php unit as a dev dependency which is why there is dash dash dev option here so let's take this go to the terminal paste it in and hit enter after the installation is complete we can open the composer.json file to confirm that php unit dependency was added and we can see here that it was added as a dev dependency within the required dev section we can then run tests using the php unit script that is located within the vendor bin directory so if we open the project files here and open the vendor directory and within the bin directory we see this php unit script so that's what we use to run the tests so if we type in vendor bin php unit and hit enter we get bunch of available options that we can use along with the command we can run tests for a specific file or an entire folder so we could specify a specific test file here for example my test.php or we could specify an entire directory and you will run all the tests within that directory or we could use the filter option to run the specific test so as you can see here there are lots of options at your disposal and you don't need to worry about all of them you will get used to them as you work on phpunit and you will probably only use few of them also if you did not want to type out vendor bin php unit every single time you could also create an alias for it to be organized we're going to create a dedicated directory for tests where we're going to place our test files in so we're going to create a new directory called tests and within the test directory we're going to create another directory called unit which will hold all of our unit tests this way we can have our unit tests within its own namespace and directory and later we could have a directory for integration or future tests and so on it is up to you how you want to structure your tests but this is a pretty common way before we create the actual test file let's first configure the php unit by creating a configuration file php unit configuration along with the other benefits helps us set some of the options that we would otherwise have to include within the command so let's create the php unit configuration file and it's an xml file in xml format so we're going to create phpunit.xml file within the project root i'm going to paste in the basic php unit configuration snippet here so let's go over it quick as i mentioned before it's an xml file so it's an xml format within the php unit we specify the options in the form of attributes so we're setting the colors option to true for color coding our test results and we're setting the bootstrap script to the autoloading script from the vendor directory bootstrap script is loaded before the tests are run and it can be set to a custom bootstrap script if needed you can check the documentation for other options that you can configure for which i'll leave the link in the description then within the php unit element we have the test suites element for better organization and because we are only writing unit tests for now we only have a single test suite element here for the unit tests because we have the unit directory here that's what this is referencing too later we could add another test suite for integration or feature tests and so on so if i open the terminal now and run the phpunite script without specifying the directory it will run through the test suites that are defined in the configuration file and as you can see we're not getting any options we're getting a warning stating that no tests were executed which makes sense because we haven't written any tests yet we're going to write some tests for the router class that we created in the preview section of the series if you haven't watched it or don't remember it no worries i'll go over what the router class does it's a pretty simple class where we have a few methods available to register different routes like a get and post routes for example they simply call the register method which adds the route along with its action to the route's property the action can either be an array that contains a class name like a controller and a method that it needs to run or it can simply be a callable then we have the routes method that returns the registered routes and we have the resolve method that resolves the route based on the request eri and the request method so it's a very basic router nothing fancy but just because it's simple doesn't mean it should have no tests so let's add a new unit test class called router test and notice the naming convention here we're following the same regular class naming convention with the test appended at the end let's click ok and we're going to extend phpunit's test case class which provides the functionality for the tests the actual tests are written in the form of methods within the test class i'm going to open the router class in a split view so that it's easier to follow and see what we're testing the first thing that we might want to test is to confirm that a route can be registered so we're targeting the register method to test if we scroll up we're targeting this method basically we can call the test method something like it registers a route so we'll do public function it registers a route and you can name tests any way you want it can be named something like route can be registered or test that route is registered and so on i personally prefer descriptive names even if they are long so it is up to you how you want to name your tests when i write tests i usually follow the given when then or arrange act assert patterns sometimes it also helps to write out what it is that you are actually testing in words or sentences for example let's write out what we are actually testing in sentences here so given that we have a router object when we take action or when we call a register method and provide the arguments then we assert the expected outcome or in this case we assert that route was registered so let's fill this in now given that we have a router object so will the router equals new router when we call the register method and provide the arguments so we'll do a router register and in this case it does not really matter what arguments we provide because we are not testing the resolving part we are testing that our route is registered so we'll provide some route here we'll do the get method and the route will be users and the action will be some users maybe controller or class and the method will be index and for the final part where we need to assert we can use one of the assertion methods from the php units test case class so if we type this assert we can see bunch of available assertions here we can use assert equals to compare the expected value with the actual value in this case we'll simply do expected array and we'll fill this in in a second and the actual value we can get from the routes method from the router object so we'll do routes because as you remember the routes method right here simply returns the routes property so now we just need to create this expected variable so let's create it right here and if we look at the structure of the routes property if we scroll up to the register method we can see what the expected value should be so let's build up the expected value here manually so we'll do get as the key and it contains a single route in this case and the key of that route is the users and the value is users index let's run the test and see if it passes and we're getting the warning that no tests were found within the router test class the reason for that is because phpunit does not know if this method is a test method or just a regular method we can hint it using annotation or we could prefix the method name with the word test let's add the annotation first so i'm going to add the annotation test here and run it again and sure enough he detected one test and one assertion and our test passed let's try with the test prefix so we can simply prefix this method with the test so we can say test that it registers a route and we can get rid of the annotation and let's run the test again and sure enough it still works it's up to you which one you want to use either way is fine as far as the naming conventions go it's also up to you and your team what conventions you want to follow if you strictly follow psr then you would probably use camel case for method names so in that case this would turn into a camelcase method name and there is nothing wrong with that a lot of developers along with some frameworks follow that convention but for me it's a bit harder to read when method names are long using camel case which is why i use snake case for test methods i'm going to revert that back to the annotation and let's continue writing some tests we can add a test to confirm that get and post routes are registered properly when we call the get and post methods now you could add two unit tests one to test the get method and the other one to test the post method or you could add a single unit test to test both some developers prefer to combine them into a single test some prefer to have it a separate test it is up to you how you want to do it but i personally like to have separate tests so for example in this case we're going to test that a get route is registered so we'll do public function it registers a get route and we'll add another test where it registers a post route we'll add the annotation to both and we'll pretty much copy this entire thing here we're going to remove the comments and instead of register we'll call the get method and remove this first argument and everything else should be the same so we'll again copy this and paste it here we'll replace the get with post and we'll replace the get method with the post method we can also replace the index with store to be more appropriate and everything else remains the same so as i mentioned some developers might prefer to test them together so you could do something like combine them and register both routes and simply build up the expected array properly and test it with a single assertion i personally prefer to have the separate tests because i want to test the get method in isolation and i want to test the post method in isolation as well so let's get rid of this here and let's run the test to see if it passes so i'm going to run the tests here and now we have three tests and three assertions and everything is passing if we were refactoring the post method here and changed it to put for example but kept the method name as post our test would be able to catch that so if we run the test again we see that now it failed because this test where we're expecting to register a post route the expected value doesn't match the actual value so then we would go in here and fix it up and run the tests again and now it's passing let's also add a unit test for the routes method to confirm that no routes are registered initially when we first create the router object so we'll do something like public function there are no routes when router is created so given that we have the router object we simply need to assert that the routes returns an empty array we can use assert empty assertion for that so we'll do this assert empty router route let's run the test and in this case i'm simply going to filter it so that it runs the test for this method only so we'll do dash dash filter and specify the test method and looks like our test failed according to the error it seems that we're trying to access the property before it has been initialized if we look at the routes method we see that it simply returns the routes property right and if we scroll up here where the routes property is defined we see that it's not being initialized and we don't have a constructor where we're initializing the routes property we're not registering any routes so the error message makes perfect sense and a simple unit test like this helped us find a bug in the code this can easily be fixed by initializing the property to an empty array either directly in the property definition or within the constructor in this case we'll initialize it within the property definition to an empty array and let's run the test again and sure enough now it passes sometimes we want to prepare some objects or some data before tests are run for example in all of these tests here we're creating the new instance of the router object the same way so we're doing a router equals new router and we're repeating that over and over in all of our tests now this usually isn't the problem when we have the simple objects like this but it becomes tricky when you are passing along some dependencies to the instantiated objects or you require some more setup of the object that is then used in most of your tests for example if we were passing some dependencies here some other objects into the router constructor or maybe we were calling some more methods here to set up the router object and we needed that for all of our tests then we would be kind of duplicating that block of code over and over again we could actually share that setup code across our tests by using a method called setup so we'll define a setup method here and this method is called before each test is run we're going to create the router object here and assign it to the router property so we'll do this router equals to new router and then we can simply replace router with this router everywhere else so we'll do router this router and replace all and we'll also add the router property and then we can scroll down and get rid of the extra lines so we'll drop this because we no longer need it we'll also get rid of these comments since we no longer need them we'll get rid of this scroll down get rid of that and technically we could also get rid of this but i'm going to leave that in because this unit test specifically tests the scenario where we're creating a new router and we're confirming that it has no register routes so yes this would still work if we get rid of this this would still work but in this case i would prefer to instantiate a new router object just in case we were doing something with the router object in the setup method so we'll do router and router or if you want to make it in a single line we'll drop that and do it this way let's clear this out and run the tests to make sure that everything is still working and sure enough everything passes we have four tests and four assertions we could also define a method called tear down which is called at the end of each test even if the test fails tear down can be used to clean up heavy objects or some external resources that were created in the setup method in this case we don't need teardown method because we're working with a simple object so we're not going to create it alright let's move on to the result method which has a bit more logic to it the first test that makes sense is to test that a route not found exception is prone if we scroll down to the result method we see that route not found exception can be thrown for multiple reasons one is when the action is not set which means that it could either be the wrong request method or the wrong route and other reasons could be that a class doesn't exist or method doesn't exist and so on also just a quick note here this check here is array check is technically not needed because we are essentially checking if the action is either callable or array and it cannot be anything other than callable or array because we have type hinted right here right here and right here so technically action will not be anything other than callable or array so we could drop this because if it was callable it would execute this block and because it's not callable then it's assumed that it's array so we could technically drop that but before we drop this i'm going to write the test so that we can refactor it and be sure that our tests still pass now we could write a separate unit test for each of these scenarios where the route not found exception is thrown or we could simply use something called data provider and provide different cases to the unit test and phpunit will run the test for all the given cases so let's create a unit test here called something like eat throws route not found exception and let's register a couple of routes here so we're going to register a post route for the user's endpoint which will execute the store method on some kind of users class and then we'll also register the get route for the users and this will simply be index now this is the given right so then what we need to do is that we need to call the resolve method so when we try to resolve a route we want to assert that route not found exception is thrown so we're going to do this router resolve and we'll pass in some kind of request uri and some kind of request method now i'm not hard coding this because if i were to hard code this then you would only be testing a single scenario right a single test case but we want to use the data provider where these two arguments here are given by the data provider before we do that though we need to assert that an exception was thrown to assert that an exception is wrong we can use expect exception method so we could type this expect exception and as you can see we can also assert exception message code and so on but we only care for the exception class for now so we'll do expect exception route not found exception and the expect exception assertion has to happen before calling the method that actually throws the exception otherwise if we did it the other way and we put expect exception after this would throw the exception and it would stop the execution so we need to add this beforehand so now we have our test set up properly we are asserting that exception is thrown the only other thing left is to provide the data provider here where we provide the request eri and request methods a test method can accept x number of arguments and it can be provided by data provider method so we could say that this test method expects request dri and request method now this of course will not work because we haven't specified the data provider yet we can add the data provider by using the annotation as well so we'll do data provider followed by the method name whose return will be passed as arguments to the test method so we'll call this route not found cases and as you can see the ide is highlighting it here because this method doesn't exist so we need to create this data provider method based on the definition from the documentation this method needs to be public and must return an array of arrays or it must return an object that implements the iterator interface which yields an array for each iteration so let's create this method here we'll do public function route not found cases and we'll return an array now this needs to return an array of arrays so basically it's an array that contains arrays as values and these array values are the basically each individual cases and whatever you provide here are going to be passed as arguments to the test method that uses that data provider so if we say here foo bar who is going to be passed in as request eri and bar is going to be passed in as request method so let's write out some failing cases that would result in route not found exception so the first two cases are when request method is not found and the route is not found we have the post and get routes for the index so the first one is easy we'll do users and put now in this case the route is found but the request method is not found the other case would be invoices and post where the request method is found but the route is not found the next case would be when the class does not exist now providing a failing case for this scenario is actually pretty simple because the user's class doesn't exist right this is just a string it's not really a class so we could provide the proper route and a proper method but it will still fail because the class does not exist so we'll do users and get and this is going to get us up to here but it's going to fail because class does not exist and it will throw the route not found exception and for the last case where the method doesn't exist for that we need to somehow simulate a way where the class exists and i don't want to hard code some controller here or something like that it wouldn't make sense because we're doing unit testing and we don't want to depend on other classes like controllers for example so what we can do is that we can use anonymous classes to kind of simulate a controller so we'll define some kind of class here like users and set it to anonymous class then within the anonymous class we'll define some kind of method let's say we'll have a method to delete an user so we'll do a delete and this returns true and now instead of using users here we could simply replace these users with users scope resolution operator class so now when we make a post request to the user's endpoint it should execute the store method on the user's class and the user's class exists because we're simulating it as an anonymous class but the store method does not exist so essentially we should get here and this should fail and you should still throw this exception so we'll add that failing case here so we'll do users and post let's run the test to make sure that it works i'm going to filter it again to only run this test so i'm going to copy that and filter and sure enough as you can see four tests were found and four assertions now these four tests are not the same four tests as the previous test result that we have this was four tests entirely for the router test class but these four tests are just the four test cases here so each individual case here is its own test if you run the test without filtering you'll see that it should say eight instead of four and sure enough we have eight tests and eight assertions now we can be confident that if we refactor this code and something breaks our tests should be able to catch such issues so we run the tests everything is still fine and if we keep refactoring and we make some kind of mistake where we only throw the exception if the class doesn't exist then this will fail because it is not throwing an exception when the method doesn't exist so as you can see we are getting a failing test saying that data set number three and the data set start from zero so zero one two three it's this one and these are the values of the arguments and the message says that it failed asserting that exception was thrown so we'll revert that back run it again and it passes so as you can see the data providers can be used to simplify tests and test against multiple cases you can even reference to external data providers say for example you had this data provider method extracted in a data provider class you could use fully qualified class name to reference this method that way multiple test classes could use the same data provider for example we could have a directory like data providers here so we'll do data providers and we could add a new class here called something like router data provider and simply extract this method to here and then we can reference it by using fully qualified class names so we need to add the fully qualified class name here so we need to do tests data providers router data provider scope resolution operator route not found cases and now as you can see within the id it's even clickable and i can click into it and this should still work so if i run the tests again we see that our test fails so let's figure out why our test is failing the error says that the router data provider class does not exist but we have created the class and we have the proper namespaces but it still says that it doesn't exist so what's the issue let's open our composer.json file and within our composer.json file we see that we are using the auto loading for the app name space and the app directory we need to do the same thing for the test directory because we have the test directory out of the app directory so it's not using the same auto loading as this so we need to add another autoloading section here so we'll duplicate this and we'll do auto load dev because we only need it for the development and we'll change this to tests and tests now we'll run the composer dump auto load let's run the tests again and everything is back to normal data providers are cool but don't try to use data providers for everything basically don't overuse it try to look at your code and see if the data provider makes sense it's useful for things like validations where you want to provide valid data and assert that everything passes or you want to provide invalid data and assert that it fails kind of like what we did here where we're providing the invalid cases and asserting that an exception is thrown for all those cases now of course this router class itself is not perfect it is not meant to be used in production this is just a simple router example that we wrote in the previous section and i decided to write tests for it because it was better than writing tests for some kind of full bar examples also in a real application you would have more complicated router because you may want to resolve other possible methods like put and delete and maybe accept the arguments within the route that he would then pass to the controllers and so on also in this case where we're throwing the route not found exception instead of throwing route not found exception here we could be throwing different type of exceptions and then we could have different tests for it to test individual cases for example in here if the class doesn't exist we could throw class not found exception if the method doesn't exist we could throw method not found exception and both of them could extend from the route not found exception and so on let's write couple more tests for the result method so far we've written tests where we're testing the failures right we're testing this and we're testing essentially this but we haven't tested this section and we haven't tested this section when it actually passes so the first test we're going to write is to assert that it resolves properly when the action is callable so we'll do something like this we'll do public function it resolves route from a closure we'll register a route here so we'll do this router get and we'll use slash users and we'll pass in the closure as the action instead of an array and we'll simply let it return one two three to indicate that it's returning some type of user ids or something it doesn't matter what we're actually returning in this case just that we're returning something that we could then assert now this is the given so when we actually call the resolve method and pass in the correct request uri and request method we want to assert that this is what gets resolved to so this is what needs to be returned from the resolve method so we'll do this assert equals array of one two and three and the actual result will be this router resolve request eri is users and the request method is get let's clean this up a little bit so it's easier to read and let's test this out we're going to open the terminal and let's run the tests and sure enough now we have nine tests and nine assertions which means that this test also passed if we commented this out for example and run the tests we see that this test will now fail so let's revert that and let's write the last test for today which should test that a route is actually resolved properly so we could again use the anonymous class for that so let's write the test first we'll do public function it resolves route and we're going to use the anonymous class here as well so we're going to copy the anonymous class from the previous test and paste it here and we'll just change the method to something like index and we can do the return of array and we can actually return the same thing one two and three though as i mentioned before the return doesn't really matter for this example because we're not testing controllers we're testing the router class and ensuring that a route gets resolved properly then we're going to register a get route for the users and we're going to use the users as our class and the index as our method and finally we'll assert that an array of 1 2 and 3 equals to the actual resolved value and that would be this router resolve request eri is users and method is get let's run the tests and make sure that it's passing and sure enough we have 10 tests and 10 assertions which means that all of our tests in the router test are passing before we wrap up for today i want to talk about the assert equals method because assert equals does not do strict comparison and it can sometimes result in false positives which can give you an impression that you have no bugs because your tests are passing but you actually have some bugs for example if we changed one of the elements from the returned array here to something like string one when we use assert equals this will still pass so if we open the terminal and run the tests everything is still passing but technically should not pass because this and this are not the same this is a string one and this is an integer one in this specific example it does not really matter because it's not the bug in our code we're creating this anonymous class within the test and we're asserting it here so it's not a big deal but if this was a method on a class within the app that was expected to return zero for example but instead it returned boolean false using assert equals would make test pass so it could result in false positives so for example if we change this to boolean and we returned false and simply think about it that this is an actual class within our app class and not an anonymous class so think about it that it's a controller maybe or something like that and we're asserting that the return value of the resolve is zero now this is going to pass if we run vendor being phpunit we see that everything is passing so you're getting the impression that everything is fine with your code but something somewhere might blow up because it's a false positive and it's better to use strict comparisons the way you can do that is that there is another assertion method called assert same and assert same in addition to comparing the value it also compares the type so if i run this again we see that this test fails and it says that it failed asserting that false is identical to zero and the key word here is identical so essentially a third same is pretty much the same as identity operator so you could think of it as assert equals as double equal sign comparison or the comparison operator and assert same is same as the triple equal comparison or the identity operator i personally use assert same in most cases and would recommend it as well just be aware that when comparing two objects a certain equals will pass when comparing two different objects with the same property values while assert same will fail since assert same would only pass if two variables were pointing to the same object and if you're not sure what i'm talking about you can watch one of the previous lessons where i talked about comparing objects using comparison operator versus identity operator and the link for that video is in the description so what i'm going to do now is i'm going to revert this back and i'm going to replace assert equals with a third same everywhere so we'll do a third equals replace with a search same and replace all and let's make sure that everything still passes everywhere we're going to run the test again and everything is still passing so this is it for this video thank you so much for watching in the next video we're going to continue with the php unit and cover things like test doubles stubs and mocking if you enjoy my tutorials please give my videos thumbs up share and subscribe and i'll see you in the next one
Info
Channel: Program With Gio
Views: 4,571
Rating: undefined 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, introduction to testing, phpunit, php testing, testing in php
Id: 9-X_b_fxmRM
Channel Id: undefined
Length: 31min 4sec (1864 seconds)
Published: Tue Sep 14 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.