How to unit and feature test your Laravel & PHP Applications

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys welcome to a new video today we are going to talk about testing um for those who have been following me for a while you know i love testing i talk about it all the time i haven't made a video about it in quite a while though i did launch my course um to deal with laravel last year but today i wanted to show you guys how to properly test your laravel and php applications how to test your models your controllers um your mailables your notifications all of that stuff and before we begin this lesson i want to talk about the differences between testing and test driven development if you don't know what test room development is it is a philosophy that basically says write the tasks before writing your code and i know lots of people dislike testing because of this aspect but i want to tell that's not the only or the right way to write tests you can and lots of people do it including mohammed from the larval um deliverable company or framework and you can write your code and write tests later i do think it requires a little bit more experience it's just easier to you know write the tests before you write a code once you get some family ready i'm sorry family family ready i don't know how to pronounce this word but once you get used to it it becomes a lot easier and you can you know just write the code and then the tests fine but for the purposes of this lesson we're going to you know go in a tdt um onto the way of doing things and i just wanted to teach you how to test your applications properly now this lesson might be quite long i haven't recorded yet and if you're interested in learning more about testing i invite you to check out my course it's called test driven development with laravel or td with laravel the link is on the description uh it has over four hours of content we talk about testing models controllers malleables notifications mocks testing third-party apis lots of things and it's a it's a very good course i suggest you check it out now let's go into the lesson so for this lesson i'm going to use a very simple laravel jet stream project i didn't want to spend much time on it you know getting things ready it's going to be a very simple project we're going to have a book model so each user can have many books and we only want two things we want people to be able to create books so they should be able to click on a button type some info and create it and they should be able to increase the sales account now you might be thinking that makes no sense and it doesn't it's just an example to teach you how to test your code so yeah let's get to it i'll see you on the screencast in fact be right back okay guys so this is our simple application it's just a jazz screen application i applied some tailwind ui styling here the free kit and we just have this table and this add bug page it doesn't do anything if i click here you know it just doesn't do anything so um i want to start from scratch if we check the code it's also pretty simple we have those sorry where is it models we have the user model and we have the book model the book model only has two columns name and price so let's take a look at it create books table so it has name um price and it has copies sold now in a real life situation that's not how you would store how many copies were sold but for the purpose of this lesson this is how we're going to do it so to start testing your applications the first thing you must um must have is php unit it comes with all our applications but if you're using a php application that's not laravel make sure that on your composer.json file you have phpunit slash php unit that's the package we're going to use to run our tests we're going to use mostly what we call feature tests or integration tasks they are tests that cover a little bit more than what we call unit tests unit tests are very very simple they should test a very small piece of code be it of a method call a method or a small function while feature tests has a couple more things for instance a feature test which has the whole request lifecycle for instance when you submit a form you're calling a controller action or a route closure and you are talking about the whole http lifecycle while a unit test would be for instance testing a small method inside a class that doesn't really you know affect much besides that specific action why a feature test is about the whole thing the whole um system as one you tested many things you can test the actions your controller takes you can test events you can test certain jobs or dispatch to the queue and it's just um it kind of provides more value um unit tests or example they're fast they're small feature tests are a little bit more a little bit slower um they interact with the database and they cover more things anyway let's get into the code so we're going to talk specifically about testing controllers so we already have this build we can have the pages but it doesn't work so the first thing i'm going to do is to go into my phpunit.xml file and right here you can see we have some commented lines if we uncomment this oh i don't know what i've done okay i have absolutely no idea what i'm doing okay so those are just environment variables you can say that we set the app m to testing we set the cache driver to array so we don't really cache anything besides the current um the current execution of code and here we set the tb connection to sqlite and we set database to memory what does this mean it means that we're going to run all of the we're going to run our migrations the whole database is going to stay in memory that means that after each task it just goes back to zero it's possible to do the same with your database of choice be it mysql or postgres it just requires a little bit more setup for this lesson we're going to use sqlite okay we can close this now if we go into tests and go into feature we have some tasks from jet stream let's open one of those for instance let's open the registration test let's take a look so we have this for sas which says test registration screen can be rendered and you can see the tests almost feel like english so it says response this get so it implies we're making a get request to this url and then we assert the status is 200. what does this mean it simply means that the framework can open this page and then we just want to make sure the http status code it returned was 200. tests are all about confirming expectations you set up a situation then you do an action and then you confirm the expectations you had were um were served test new users can register same thing sends a post request to this url with this data and then um it asserts we gotti redirect to a certain route so let's create our own tasks first we want to make sure this page can be rendered i personally like to put my tasks in the same folder as they are in the app directory so if we have our controller under app http controller's boot control i would like to have it under feature http controllers boot controller test this is not the right way there's no right way really um you can see that those are just called feature tests and they are just you know very generic registration tasks it doesn't really mean it doesn't really refer to the reactor controller it's just called registration tasks but for this last we're going to go with the same path as the class we are currently testing to do this i'm going to open up my terminal let me clear this and i'm going to type php artisan make test and i am going to say http controllers book controller test if we don't specify anything laravel already assumes we're talking about a feature test but if we want to make a unit test we can place the dash dash unit flag so let's remove this and let's run this okay task created successfully if we go here we can see our folder in our tasks let's open up let's let's remove this so it already comes with an example we can get rid of this chalk block so let's start by renaming this one we can say test the books index page is rendered properly something like this okay and well let's let's before we're writing code let's you know just think what we want to do here so first uh we want to create a user okay then we want to hit the books page then we want to assert that we got status 200 okay let's start so to create a user we are going to use something called factories they ship with laravel and they are used to create a bunch of fake data how do you use it it's pretty simple first if we search for user factory you can say we already have this file it's under database slash factories they have a model in this case user and they have a definition and this is just the fake data it is going to return faker is a library that gives you fake data so we're getting a fake name a fake email the email verification date is going to be now and then we just have a password which is password in plain text and a random remember token so let's create a user you can say user means user let's import it factory and then we can say create okay we have a user now we gotta hit the books page so let's store the response in a variable and we can say this get books that's it that's all we want to do and here we want to make sure you got it 200 so we can say response assert status 200 let me get rid of this otas okay and to render since we only want to run this class i'm going to copy this i'm going to open up the terminal and i'm going to say php artisan test dash dash future and passing the class name we don't want to run all of the tasks let's run it okay you can see it failed so it said no such table users that's because we are not using the refresh database trade and this is the trade that's responsible for creating and then deleting the database since we're talking about any memory database if we were talking about a mysql or postgres or a real database it would um run all of those inside a transaction and then revert the transaction so you always end up with the database in the same state that it was if you had no data in it it goes back to having no data in it let's say use refresh database and let's rerun this okay so expected status go to 100 but received 302 so it wasn't able to render the page and the reason is we're not authenticated but if we go into our routes we can set the books routes are inside and off middleware to um to impersonate a user to act as a user it's very very simple all you have to do is call this actingness and you pass a user object if we run this again okay we are green it works now if we were to go into our boot controller and you know i don't know let's pass a typo here let's say use it with two hours and let's run the test and you can see it failed we got it 500 now this error it isn't really you know descriptive we don't really know what's going on here and that's because the error is getting past the exception handler which is then rendering a 500 page laravel provides a helper to let us get the actual errors on the test all we have to do is call this without exception handling now what's the catch here the catch is if you're testing validation or anything that relies on the exception handler the task won't behave properly so if you're testing something that relies on validation don't use without exception handling let's run the same test now and now we have the actual method so request card user with two hours did not exist let's fix it and rerun it and we are back screen okay so we're testing something that already worked so why why do we test this um testing has many use abilities but one of the one of the the you know the reasons i like testing the most is because it provides you with a lifetime guarantee that something works so i wrote this test now i spent you know one minute two minutes on it and as long as i have it every time that i run my test suite it is going to confirm the book's page is rendered properly and i think this is wonderful but here's a catch we always have edge cases and um it's good to think about them before you write it as sometimes you can't think of all the edge cases and you just catch them as you go for instance a user reports a bug in your app and that's fine that's going to happen and once it happens instead of just going and fixing the bug you can write a test for it you can try and replicate the bug on the test the test is going to start failing and after you fix the bug the test should be passing so now you have another contract that that specific bug won't reappear if it does the test is going to fail so this is one of the reasons i like testing so much let's keep going so we have this very simple test let me make this smaller but i want to test people can create books so let's create a new task let's say tasks users can create books let's go back let's start with english and we can say first we want to create a user then we want to hit the books route url sorry with a post request and then we want to assert we were redirected to the books page that's it first let's create a user we want to act as a user so let's say this acting as user we want to hit the books url so we can say response equals this post and we can say books that's the route we're going to use to create books and we have to pass some data right so we can say name um let's say 1984 the price it is going to cost us 29 bucks to purchase this book and that's it now we don't just want to assert we were redirected we also we want to assert the book has the proper data right so first let's go here and say response assert status 302 now we want to assert the book has the proper data and we're going to use speech p units built-in assertions like i said testing is simply a matter of confirming expectations so we can say um assert equals so what we expect for the name we expect it to be 1984 right and then we can say book name the price well the price we expected to be 29 and we can say book price and the sales count which is something we didn't pass well that needs to be zero right so book sales count let's be real another thing is we must ensure that the book belongs to the user that made the request so this is also pretty simple you can say this i'll start equals and here we can say something like user id and then we can say book user id we're just confirming this is just a you know a true statement an equal statement that's it if we want to go further and we want to make sure we actually got an instance of a user we can use something like assert instance of like this and we can say user class and then we can pass the user object from the book okay now i might be thinking where's this book variable coming from from nowhere we gotta create it so we got to find the last book created since we are only supposed to have one book it should be the last book and we can say book first let's make sure we only have one book in the database so you can say assert equals one book count this is just eloquent it's just laravel and okay now we have a proper test so i'm going to copy the test name and we're going to go back and say php artisan test dash dash filter and the method's name okay we can see it is failing so expected status quo 302 would received a 200. if we go back into the controller you can see we're not really returning anything for instance let's do a dump and die here and just pass one if you run this oh sorry i ran a shortcut let me run it again we get the one which is pretty cool since we want we won't really test validation here let's add the without exception handling here okay let's get rid of this and now we can start implementing our code so let's start with something half-assed let's just say redirect to the books page let's see what happens um so check this out it is failing on line 51 which means line 45 is passing but well failed asserting that zero matches expected one we expected to have one book but we got zero so we have this feedback loop generated for the tests they kind of tell you where you need to go what's wrong and what you need to fix it so obviously we gotta create a book so you can say book i'm sorry let's say auth user books and we want to create a book right and we can say that the name is going to be the request input name and the price is going to be the request input price and the sales count is going to be zero like this let's try running this now add name to fallible property to allow a mass assignment so can you see how the tasks are guiding you on what you need to do let's go into the book model let's add the fillable property followable and you can say name price and sales count let's run this again it's failing so table books has no column named sales count oh let's see what i mess up here create books table copy sell it's not sales account it's copies sold so let's update it here copies sold and on the book model as well it should say copy so let's rerun the test okay i'm trying to get property id of non-object which means this right here is returning no or in something that's not an object and the reason is also pretty simple we don't really have the book method now this is level magic that's why it's a little bit the error is not so clear if you get someone who never worked with laravel it might not make much sense but if you work with laravel if you've ever used eloquent this is pretty clear for you we don't have a user method here let's create it user return belongs to user class okay let's rerun this as you can see we now have a passing test so if we run all the tests for this class let's copy this php audience test filter we have two passing tests which is pretty cool now remember i said we wanted to uh add something here to increase the sales count and the reason i want to add this is because i want to dispatch this as a job to the queue is it a brief example definitely not but i think it's something that you guys will benefit from how do you test that something was dispatched to the queue first um once someone adds a book i also want to notify the user okay so how can we task that a notification was sent it's pretty pretty simple the first thing we got to do is to fake the notification handler how do you do this call notification let's import the facade and call fake that's it now you have a fake notification handler after you assert everything you can just say notification assert sent to and you can pass notifiable which is the user and now you got to specify the notification we don't have one so let's create one we can say make notification book created okay so let's say book created here and pass the class and if we rerun this test you can see that it's failing the expected book created notification was not sent so we gotta implement this let's just say off user notify new book created like this let's rerun the task and you can see it passes now fakes are pretty much like mocks um we are testing that something was sent that a notification was sent but we are not testing the notification itself in fact the notification doesn't have anything cited it just has this you know generic message this is not what we want so a notification task would be a separate test if you are interested on this i have videos about this on my channel but i'm not going to particularly cover this on this video i may make a new one but i'm not going to cover it on this video so we are just testing the notification was sent we're not testing the notification has the proper content or even that the notification renders for instance um if we were passing a variable to test access the notification would fail and our task would still pass that's because it's not really the responsibility of this task to make sure the notification works properly all you have to do is to make sure the notification is being sent okay we can create books that's very cool now let's create a functionality that allows us to increase the sales scout um i am going to make this on the same test file but i would suggest that on a real app i like to there's a really cool talk about adam i'm sorry by adam weather called credit by design and he basically says that each one of your controllers should only have the um the restful verbs actually i'm not going to violate this i'm going to create a new route here and i'm going to say route post books so a book here and say sales something like this and then we can say we can create a controller so you can say book sales controller and we can pass book sales controller class store something like that and let's write a test for it as well so make test remember http controllers book sales controller test let's go to it book sales controller test and here's another cool thing about that um once you already have a couple tasks written you can just really copy what you had on the previous one so let's copy this oh sorry this is what i want to copy so use refresh database let's get rid of this and let's say test it increases a book sales let's paste this we don't really care about notifications here let's say let's import this okay um so here's what we want to do differently um we want to have a book already so let's say create a book and how do we create a book do we make a post request to that endpoint like we do on the other test no that's not what we want to do we can say book equals we can say user books and we can say save what's the difference between save and create create expectant array while save expects an object and we can say book import it factory make pretty simple right um so we have a book we want to make make a request to the sales endpoint so let's say response equals let's post books and here we have to pass the book i t right so i'm going to change this to our double columns like this and you can say block id and then sales we want to make a post request without anything without any data that's all we want to do we want to assert we had a redirect and you cannot only assert the status code but you can even insert the url so it should resurrect so you can say books like this and then we want to make sure do we want to make sure the book um copy so it was increased no since i remember talking about using that job for this now i might be thinking why would you use a job for this this is stupid because this is a gsa scenario that it built in 10 minutes but in a real application sometimes you want to dispatch things to the queue so the user can get a response right away or sometimes it takes so long to do something that you have to dispatch to the queue otherwise the request you would time out so um let's assert a job was dispatched how do we assert that a job was dispatched well first we gotta call buzz the facade fake then we can go here and we can say burst not burst bus assert dispatched that's it and we gotta have a job so let's create a job make job and we can say increase book copies so like this so we can say assert dispatch to increase books episode now this already asserts that this job was dispatched how can you make sure that you dispatch the job for the correct book you can pass a closure that accepts a job and you can say return you can pass a condition here so you can say that your job book id should be equal to the book id like this let's just use the book variable right here okay um i think we have a pretty satisfactory task right now let's run this so i'm going to copy this and i'm going to say php arts and test filter okay so it's failing that no concern fail box name so remember we haven't really done anything on this factory let's fill it let's say the name should be um this faker name i can't remember if it was a matte oh yeah it was a method um sentence actually maybe maybe that would be best and the price can be uh i think it's random number and we can say just in the number of digits so let's say one and of course the sales the copy showed is going to be zero let's rerun it it's failing so it says book sales controller is sword does not exist again we are on that feedback loop um book sales controller let's open up oh we don't have anything so let's create a store method that accepts a request okay let's run it now um failed asserting that 302 is identical to 200 so we are returning it to 100 response simply because we are not returning anything so let's start by redirecting the user to the books page let's rerun it um so the expected uh increased book copy sale job was not dispatched that's what was missing so we can say increase book cops so dispatch and we can pass the book how do we get the book we don't have the book let's run it undefined variable book well since we're passing the book on the url and we have a variable on the route we can use route model binding and we can say book look like this if you don't know what broad model binding is i do have a video here on the channel check it out let's run it um undefined property app jobs increased book cop cell book so the reason is we don't really have any property called book here and we are not we're not even passing the book through the constructor so let's let's um add this so we can say public book we expect an instance of a book here and you can see this book equals book let's run the test and it's passing so be careful we are testing that the job is dispatched we are not testing it works so just to sum this video up let's test this class well let's write a test for it we say make test we can say jobs and increase book copy so test let's copy here all that we've done here increase book copy so test let's get rid of the stock block and we can say task it increases the copy so let's space this so let's import this now this test a little bit different since we're not passing anything through um the request lifestyle we're not really we don't really have a request to work with here we're not going to use any of the request methods um this is what i like to call a unit test even though we are going to use the database so use refresh database some people might not call this a unit test because it interacts with the database but i like to call it a unit test we are just testing there's a specific um class and a specific method to be honest so how can we test this well it's pretty simple all we have to do is to actually execute the job and then confirm our expectations so let's say execute the job and then confirm the copy so amount increased okay so the first thing i want to make sure is that we are starting from zero so you can say zero cop is sold you can see cops so count or just episode it's just copy soda so we want to make sure we're starting from zero we want to execute the job and then we want to confirm we got the correct amount of copies so any time that we execute something that updates a database we have to call book model refresh this makes sure we're getting um the newest data from the database it refreshes the model and then we can say this assert equals one book cop so why one because we are only going to execute the job once to execute the job very very simple we can just say increase book episode dispatch and whatever arguments you pass to dispatch it just passes those to the constructor so another way to do this if you don't really want to rely on the magic is to say new increased book copy code pass the book and call the handle method but i like to do it like this okay let's run this as php artisan test filter and it's failing now see how fast it was zero point fourteen seconds so failed asserting that zero matches expected one okay then there's something wrong here let's implement it so we just want to say this book increment is that increment i can remember i think it is cop is sold let's run the test now and it's passing so now we have um it working if we were to do it twice it would fail if we go back to one it passed so if for some reason someone at your team you know puts a typo here and say copy so the task is going to fail and you don't you don't have to test it manually you just have your whole test with working for you so this is how you start testing your level applications let me know if this video made sense for you guys i'm going to share the code on github and i'm going to leave the link on the description if you are interested in learning more of this you can drop a comment on this video and um i can maybe work on another video but if you really really want to go um deep and test and learn how to properly test your layer of applications i really suggest that you check out my course you can get five lessons for free to see if it fits you and well you know testing is one of the most important skills a developer can have in these days so it's really important that you know how to test your applications make sure to check to deal with level out make sure to subscribe to the channel and make sure to follow me on twitter that's where i post most of my news um that's where i'm the most active at really thank you for watching the video thank you for subscribing thank you for um being if you've been following me for a while thank you for the trust i know it's you know um these days it's really hard to follow a bunch of people um so that was really it wasn't the best wording but what i mean is um i don't follow anyone so i highly appreciate you guys following me um what he meant to say is i only follow people i actually like i actually enjoy so thank you all for following me here on youtube or um twitter or the newsletter or anything like that um thank you so much have a great weekend and uh i'll see you on the next video see you later bye bye oh i forgot about this one of the most pleasing things on testing is just running the whole test we check this out php arts and test i love this i love this okay thank you for watching and i see you in the next video bye bye
Info
Channel: Mateus GuimarĂ£es
Views: 6,022
Rating: 4.9471364 out of 5
Keywords: php, laravel, tdd, test driven development, clean code
Id: WNkNZbKsrH0
Channel Id: undefined
Length: 40min 2sec (2402 seconds)
Published: Wed May 05 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.