"Test-Driven Laravel" – Adam Wathan – Laracon US 2016

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Hey how's it going good morning Larrick on 2016 it's everybody excited to be here what are you guys ready to look at a lot of code this morning I don't really do slides so I got a couple here but that's mostly gonna be code so this talk is called a test-driven laravel we're gonna spend kind of an hour and a half talking about my approach to test-driven development in laravel and kind of show you how to go from a brand new application that has absolutely nothing done to it not a single line of code written you know no configuration specified or anything and drive out some features completely test first without writing a single line of code first so for the slide portion I want to talk about test-driven development in general a little bit and kind of give a bit of an introduction to the idea of it for anyone who's not super familiar so at the core of it test-driven development is about this workflow this TDD loop where you start by writing a failing test then you do whatever you need to do to make that failing test pass and then once you have that failing test passing you can refactor your code until you're kind of happy with the design and happy with how it looks all while trying to keep the test screen and keep everything passing the whole time and then when that's done you move on to writing your next failing test and working on the next feature so kind of a classic example is something like this where maybe you're trying to build like a little calculator class and you're trying to use a test-driven development kind of unit test this calculator so here we have a test that's checking that you can add two numbers with this calculator right so we knew up a calculator we call this add method we pass it two parameters five and seven and then we assert that those added up to 12 so the first thing that you need to do here is run the tests and see what the error that you get is so we get an error something like this class calculator not found because we're doing TDD we haven't written any code yet so there's no way that the calculator class can exist so following the errors the first thing that you would do is define the calculator class and that's all you have to do then you run the tests again and now we get an error saying that there's no add method because you know our class was empty so we go and add an add method we specify the parameters that we kind of had outlined in our test and we run the test again and now we get another error saying that we expected 12 but got null because we're not returning anything from that function so now you go and actually write an implementation for your add method run the test again and finally everything is green so this is kind of like the classic way that I see test room development introduced to people kind of at the unit level where you're driving out these like small isolated components that have no dependencies that do really simple stuff and when you're trying to learn test-driven development this way it sounds really easy like it sounds like oh yeah that sounds like a cool way to do things I think I can do that but then in my experience when you go to build like an actual application and you try and put this stuff into practice all of a sudden that seems a lot harder because you have things that are like making Network requests to github we're like generating PDFs or hitting the database all sorts of stuff that you can't just do in like a little isolated class with no dependencies so the first thing I want to talk about is the workflow that I use to kind of like get the ball rolling with TDD in an actual application instead of just like driving at these little isolated units that to me are not really representative of what it's like to build this stuff out in the real world so we're gonna be building an app called tweeter it's probably similar to an app that you might have heard of it's a little bit better though because the tweets go to 141 characters it's like the spinal tap of Twitter so what are some of the you know we got to figure out what is the very first test that we need to write for this application this is kind of like the classic point where someone gets stuck if they're trying to like do TDD for an application you kind of want to do it right from the very beginning you don't want to like write some code and try and decide like okay well you know how much code am I allowed to write before I start writing tests can I at least define it route can I configure my database stuff like that and my opinion is know if you want to do test-driven development properly write a test first and kind of let that drive everything out and we're gonna talk about how to do that so when you're trying to figure out what your very first test should be you have to think about like you know what are the features that this application is gonna have so what are a couple things that you might be able to do and tweeter well create an account that's something that you're probably going to need to have to do post a tweet view someone else's tweets like go to someone's profile page and see what they've been talking about follow another user so their tweets show up in your timeline or view your timeline and kind of see the tweets that all your followers or the tweets of the people you are following have been tweeting so how do we go about deciding which one of these to start with when we're working on a brand new application well there's a book called growing object-oriented software guided by tests written by NAT price and Steve Freeman and when they're faced with this problem trying to figure out what the very first test should be in an application that they're working on they talk about this idea of trying to string up what they call a walking skeleton and a walking skeleton is basically like trying to do as little as possible to kind of string the whole app up so that you have everything that's kind of needs to be set up for like requests to work kind of in place so that you can kind of like hit an end point have it cut through the app as much as possible and come back to the user something that kind of proves that things are in place and you have a structure to kind of be working with so keeping this idea of a walking skeleton in mind let's look at the features that we talked about and kind of evaluate each one of them against our goal here and see which one makes the most sense to start with so creating an account creating an account what's it gonna involve it's gonna involve some sort of UI so we're gonna have maybe like a sign up endpoint and form that someone fills out and submits and that's gonna create a user account and store it in the database and then maybe let them know that they were registered successfully something like that so that does sound like it's gonna like cut through the whole app in terms of right from the UI to the backend and all the way back to the user so if that process works then we've kind of verified that we've got something up in place but in my opinion it's kind of like sidestepping the whole point of the application which is like tweeters about tweets and being able to view someone's tweets and posts tweets creating an account is something that you do in every single application well a lot of applications and I would rather start something that like is more focused on what the app is actually about if possible so I'd prefer to cross this one off for now and see if there's something else that fits our criteria a little bit better so posting a tweet so posting a tweet again there's gonna be some sort of end points some UI a form you're gonna fill it out and it's gonna store something in the database and kick stuff back and it's about the app but posting a tweet is kind of a core thing so it sounds like a pretty reasonable candidate but is there any kind of complexity that's gonna get in the way in terms of slowing us down and adding time to how long it's gonna take for us to get this walking skeleton up in place and I think a little bit of complexity I see there is that it kind of depends on people having accounts and being registered right you don't want people to just be able to post tweets without having an account so to be able to post a tweet we need to worry about authentication and having users created and stuff and it'd be nice to avoid that if possible so let's cross that one off for now and talk about some other stuff so viewing someone else's tweets so again this is gonna kind of cut through the whole app is gonna be some endpoint that you have to hit you're gonna be pulling some data from the database and kind of displaying it on the screen but the nice thing about it is that you don't need to be authenticated to do this so it kind of lets us avoid the whole authentication aspect and still get to some stuff that's related to the core of the app which is tweets in this case so I'd like to start with this idea of being able to view someone else's tweets I think that's a reasonable place to start it's probably plenty of other places to start but it seems as good as any in my opinion so that TVD loop we talked about before where you just are writing a failing tests trying to make it pass then refactoring and working on to the next failing test I find when you're trying to build an actual application you have to worry about you know testing the user interface and stuff like that things are a little bit more complicated than that and I use an approach called outside in test-driven development or acceptance tester and development that looks a little bit more like this so the first step is to write a failing acceptance test something kind of from the end users point of view that kind of defines a outside-in sort of blackbox behavior of the system then you work on making that test pass which often involves driving out unit tests for smaller components and those past with this kind of inner TDD loop and once all that stuff is working hopefully your acceptance test is now passing and you can do any kind of high-level refactoring you want and then move on to the next acceptance test so this is kind of the workflow that I like to use and that I would like to go over today so enough with slides let's write some code so I've got a brand new laravel application here that doesn't have anything edited at all just kind of laravel new with the dependencies installs and stuff so we don't have to worry about depending on the network or anything and I want to walk through how we can implement this feature of viewing another users tweets so the first thing that we're gonna do we're not gonna configure our database we're not going to create any routes anything like that we're gonna write a test so what should this test be called and where should we put this test a sidebar is a little bit small here but I'll kind of talk about how I'm organizing this stuff what I like to do when I'm writing a new acceptance test the way I like to keep my tests organized these days it's kind of simple I kind of have like a features folder in my tests folder and a unit folder and the features folder is where I'm kind of doing these acceptance tests and the unit folder is where I'm doing tests for individual components I've seen well people do a lot more complicated setups than that in my experience this kind of kind of simple differentiation between the two has worked out pretty well and it's something I've been using for quite a while now and been pretty happy with it so the first thing I would want to do is create a new test in a new folder called features in our tests folder so let's just start with this example test that laravel provides out-of-the-box and we'll just move this to features slash and now we'll rename the test to something like view another users tweets test so we have this file now let's rename the actual test so we'll call it view and other users tweets tests and we'll kind of delete this stuff because we don't need it and we don't need this comment and let's kind of figure out what we need to do so the test itself is gonna be like test can view another users tweets now let's think about what we kind of need in place for this to work right so to be able to view another users tweets we're going to need to have a user that has tweets right so we're gonna have to have some sort of user in the system that user is going to need to have tweets and then to be able to view that user's tweets we're gonna need to like go to their profile page right so maybe visit that user's profile and then we want to see their tweets so this is kind of like the outline of what we want to be able to do here in this test so let's kind of walk through filling in these comments one at a time with some real code and see where it takes us so having a user in this system how can we have a user in the system kind of ready for us to work with well there's a couple ways that we could do it first way that you might think of especially since we're doing this kind of acceptance test-driven development this outside-in thing is you might think okay well maybe we need to go to a registration page and register that user and then go to the tweets page and post a tweet as that user and then log out so that we're a guest in the system and then go to that person's page and view their tweets that's certainly one way to do it I think it's not the best way to do it in this case because it's going to get in the way of our kind of walking skeleton and it's also testing a lot of things that are not really related to this test so if there's a way that we can kind of avoid all that stuff and do it a little bit more directly I think that will help us out so what I like to do in these sorts of cases is use an approach that is referred to in the our spec book which is like a book on using our spec and behavior driven development in Ruby and rails applications and they call this approach direct model access where you're creating models that you need in your tests directly even in acceptance tests to kind of set up the world that you want into kind of a fast and direct way and then kind of act around that data and make assertions about it so how can we create a user so we can do something like user equals and now laravel as of I think 500 or 5-1 maybe it ships with a feature called model factories which are some testing helpers that let you kind of generate eloquent models really quickly in your tests and let you avoid kind of a lot of duplication and stuff and kind of only specify the stuff that's important for what you're trying to do so it looks kind of like this to do so we're gonna do something like factory user class and we're gonna create that user in theory that should give us a user and now the next thing that we need is tweets for this user right well I think multiple tweets maybe is gonna slow us down a little bit why don't for now we just worry about a single tweet and that'll kind of help us get this walking skeleton in place so instead of tweets let's just create a tweet so what does it look like to create a tweet well it's gonna look kind of similar now I'm gonna reference a class here that we haven't defined yet and the reason I do that is because I'm using this approach that's also referred to in the growing object nor did software book as programming by wishful thinking where everyone was lost of that but I think it's kind of a cool idea basically the idea is that um you're writing code that you wish existed or expect to exist or hope existed and you let the tests tell you whether it does exist or doesn't exist you just kind of write the code that you wish you had and run the tests and if the tests say oh dude you haven't defined that yet then you go ahead and define it but a lot of times stuff does exist like for example this user class laravel ships with one out of the box but this tweet class it doesn't so we'll create a tweet and we need this tweet to belong to this user right so what you could do is you could do something like this create and then here you could specify you know how do we want to set this up well I think a tweet should belong to a user if it has a user ID that matches the users ID and then maybe we need like some sort of content for this tweet right so maybe we have like a body on the tweet that's something like my first tweet now this will work but I don't really like the idea of referencing like foreign keys and kind of leaking this information but like what the column names and stuff are that manage those relationships kind of all over the application I think there's a simpler way to do it and what we can do instead is we can use make instead of create so create is creating a user and storing it in the database make is just creating a tweet in memory that's almost ready to be persisted but isn't persisted yet and then we can save that tweet by calling user tweets save tweet and again none of this code exists yet right we're doing this programming by wishful thinking thing just kind of writing the code we wish we had and we're gonna let the tests kind of tell us when we have something that we actually need to implement so visit the users profile so we can do that by going this visit and then just decide what we want the endpoint to be in this case I think we'll just put it at like the person's user name directly hanging off the root of the app kind of like Twitter does so we're gonna need this person have a user name right so let's specify that their username is maybe John Doe and then we'll visit John Doe and now we want to be able to see their tweets or in this case see their one tweet and we can do that just by calling see my first tweet so this is kind of our first simplest most basic test and now we just need to run this and watch for errors and try and get it passing and again remember we're starting from like a very fresh nothing in at laravel application so we're gonna run into a lot of stuff that's just kind of basic setup stuff but it's interesting in my opinion to kind of see how we can use the tests to force you to create migrations to force you to find routes stuff like that so we'll run this test we get an error unable to locate factory with the name user and in that case this is because the user class is named space and we haven't imported it so we'll import the user class run the test again now we get an error access denied for user homestead at localhost because it's trying to create a user in like the database that laravel ships with out of the box like in its environment configuration which is the homestead stuff we don't really want a real database like set up in home set or anything for this so we can use this information to kind of help us drive out our database configurations how can we fix that error well I like to use a in memory database whenever possible like a sequel light in memory database for tests sometimes you outgrow it but I think as a starting point it's a reasonable thing to try and the way that we can do that is by adding a few environment variables to our PHP unit configuration so down at the bottom here we can say DB connection should be sequel light and then our DB database which is usually the file name where you want the sequel Lite file stored if you use : memory : it's just gonna create one in memory at the beginning of every test that's not actually saved anywhere I'm just kind of discard it at the end so if we run these tests again we get an error no such table users okay so that means we have to migrate our database right so the easiest way to do that in laravel is to just use this database migration straight so if we type use database migrations run the test again now we get an error Table one or sorry table users has no column name two username so laravel does give us a user table migration out of the box with some stuff defined and we need to kind of tweak this to kind of meet our needs here so I'm just gonna kind of delete the email I'm gonna leave password remember token and time stamps because well everyone's kind of configured to expect some of that stuff and I don't want to get too deep in the weeds there so just kind of delete what we can but let's change this used or this name column to be username so that we can kind of get past this error of missing the the name column so table users has no column named name huh that's interesting so why does have no column name name if we don't look at our model Factory which is where the default user factory is defined by Larry Bell you can see it's still trying to fill in stuff that we're no longer using so let's use user name instead of name and we can use fakers username generator we get rid of the email again we'll leave the password remember token just to avoid getting too deep in the weeds and run our test again and now we get an error unable to locate factory with the name tweet so this is actually significant progress because we finally made it past this first line which means we have a user created and that users in the database so if we were to go here just like dump that user out and run the tests you can see we actually have a user in the system now which is pretty cool and we did all of that for TDD we never made any changes about the tests kind of telling us what change to make so we have no factory for tweets so let's create a tweet factory so we'll duplicate this factory for now and we'll point it at app sweet class and we just won't have anything in there because we want to kind of let the test kind of guide us here if we run the test again you get an error unable to locate a factory with the name tweet again and the reason for that now is that we haven't imported this tweets namespace so we'll just duplicate this line here because we're expecting that this tweet should be in the app namespace run the tests now we get an error that there's no tweet class ok so following the errors let's generate a tweet class so we can go artisan make model tweet and now we have a tweet eloquent model kind of scaffold it out for us run the tests again call the undefined method tweets on the query builder so what's happening here is we're trying to call tweets here to save this tweet on the user and it's noticing that there's no tweets method defined on the user so it's kind of checking a couple kind of just in case locations and the query builder is kind of where it ends up the end of the day so what this is telling us is that we need to define a tweets method on the user so let's go to our user class and down here we'll just add a method called tweets and this is just gonna we'll just leave it empty for now run the test again call to a member function save on null so it's trying to call save on the result of tweets which is nothing in this case so let's set up our relationship so this has many tweet class run the test again no such table tweets ok making progress now we need a migration for our tweets so artisan make migration creates tweets table create equals tweets oh this is an error that me and Taylor ran into yesterday if we just jump composer and run it again we should be good ok so we have our tweets table migration run the tests again tweets has no column named body ok so let's go to our create tweets table migration and we add a body so table string body run the tests again no column user ID so it's pretty cool right these tests are really kind of telling us every single mistake that we're making and all we're doing is kind of responding to the errors that the terminal is kind of driving out for us so if we head back to our migration again we can add that foreign key so to keep things simple we'll just you know add a user ID make it unsigned run the tests again and now we get some sort of more interesting error so a request to localhost / John Doe fail that received a 404 so what this is telling us is that we haven't defined a route so now we can head to our routes file and define our very first route for our application so if we go to routes web I'm using a level 5 3 here so a couple little bit a couple changes here so now your routes file by default is in the right slash webbed up here instead of revs up the HP we can define a endpoint so this is going to be a get request to someone's username and now we could try and do all this work in this closure but to me that doesn't really help us get to the design that we inevitably want to land on no I will make sure not to touch that cable for the rest of the presentation so what I like to do is kind of use that programming Oh programming a wishful thinking kind of approach again where what kind of write code that we wish we had and let the tests kind of tell us when it does or doesn't exist and kind of fill in the gaps as we go so where would I like this to kind of go how would I like this to work I think ideally I would like this to be something like the show method on the user's controller so if we run the test again now we get an error that there is no users controller so we can run artisan make controller users controller run the tests again some messy error here we received a 500 because users controller has no show method okay thanks tests let's create a show method so we head over to our users controller that's newly generated and define the show method we'll just leave it empty and run the tests again now we get an error that the current node list is empty and what this means is that in this test we're doing this kind of C assertion it's trying to inspect the HTML that came back and find the text that we're looking for so the way it works is it kind of crawls through all the HTML dom nodes and looks for the text and in this case we aren't returning any HTML so can't find anything to check against so again programming by wishful thinking how would I like this to work well I would just like to return the users not show view run the test again now we get an error that there is no users our show view okay let's create a user's dot show view so if we head to our resources views folder we'll create a new folder called users and in there will create a showed up blade that PHP and we'll just run the test again we back to that error of the current node list being empty so why is it empty now well it's empty because the blade view is empty so programming by wishful thinking let's kind of just write the code that we wish we had and let the test kind of guide us so maybe we wanted to do just like for our basic implementation it's like an unordered list where we loop over the tweets and we stick each one in like an Li and we just do like the body of the tweet in there right go run the tests again and now I'm expecting an arat about that variable not being defined in the view right but we'll let the tests kind of tell us that so scrolling up to this big nasty error we get undefined variable tweets just like we kind of expected so that means we need to pass in a tweets variable so let's just say that the tweets should be our tweets variable in the controller which also is yet to be defined but we'll let the tests kind of let us know about that so we get the same similar error here but now the undefined tweets variables in the okay so heading to the controller now we have to think like how are we gonna get these tweets so what you could do is you could say something like you know tweets equals user tweets and run the test again and and see what happens well it's gonna say something about that user being undefined right okay so how do we get the user so now we have to do things that are a little bit more interesting so to find that user we could do something like this we could say like user equals user where user name is user name and make sure that we take that as a parameter to the rent and then get the first one and then get their tweets and then we'd be kind of good to go right but again this is a situation where to me this isn't helping me get to the design that I think I would like to land on where I don't really want to just chain a bunch of where clauses stuff in my controllers ideally I'd like to come up with some more expressive methods that kind of you know Express what I'm actually trying to do so what I might do instead is something like this user find by username pass in the user name and run the tests again and now we're gonna get an error the users not found okay so we got to import the user class in our controller so import happy user run the tests and I'm expecting an error or something about not having that method defined so we have no find by username method so we could kind of just let the acceptance test here work as an umbrella that covers this and let us drive out this functionality or we could use this as an opportunity to jump down to the unit level and write out a unit test for this function which i think is a good way to go so let's create a new folder in our tests folder called the unit and then in this unit folder we'll create a user test about PHP and I'm just going to copy this test in to get some of the boilerplate in place for us so we'll just call this user tasks let's kind of get rid of stuff that we don't really need yet and figure out what we want to all this test so something like test can find users by their username so what does this test gonna look like well we're gonna need a user in the system to look for right so we can do that kind of the same way we did before so we can say user equals factory user class creates and we're gonna want to specify their user name so that we can find them so maybe we look for Jane Doe this time and then we want to look up the user by their user name and kind of verify that we got the right one so let's call this created user and we'll say found user equals user fine by user named Jane Doe and then we want to just make some assertions to make sure that they are in fact the same user so maybe we want to verify that their IDs are the same so we could do something like assert that the created user ID is the same as the found user ID and maybe we'll just verify that the user name is what we expected as well so we'll make sure that the found users user name is Jane Doe run this test and we'll just kind of let TDD guide us through the process here no such table users okay so we got to use our database migrations like we did for the other test call the undefined method find by username so now we have an opportunity to actually drive this method oh so we head over to our user class and maybe right here we can add a new static method called find by username that takes the user name as a parameter we'll run the test again trying to get property of non-object so what this is complaining about is that you know fine by user name is not actually returning a user yet so let's drive that out we can use that implementation that we have kind of talked about directly in the controller write something like return self where username is the username and moves to get the first one if you run this test we're green now if we head back to our acceptance test and run it it's also green so that was kind of an example of doing that outer TDD loop that forced us to drive into that inner unit testing TDD loop drive out a unit test for this user class and ultimately get both the unit tests and the acceptance test passing so now we've kind of implemented that first feature that kind of gets our walking skeleton in place without ever having to open the browser without ever having to like just kind of blindly write code everything was done test first which i think is a pretty cool way to do things so I have another whole feature that I want to drive out but I think maybe it would be good to take a break for a minute or two and just see if anyone has any questions about kind of what we just walked through there so if anybody has any questions like kind of make yourself known I think there's some mics somewhere you might even be able to just walk up to the mic if anybody has a question hi yeah sure any particular piece that you're interested in cool so it's just these two lines here so what we're doing is we're specifying that for the DB connection we want to use sequel light and if you check out you know config database PHP that's just specifying which connection should be used as the default connection so that's going to look up this driver here our sequel light connection story and then that connection is looking for an environment variable called DB database to specify what database to use or defaults to kind of database to us equal light and we're setting that environment variable to : memory : which tells sequel light to run an in-memory Sall cool any other questions or yep step-by-step here there's a lot of totally political is it that you run the test every single small change is that I would say it's a good idea to run the tests frequently after every small change while you're learning and kind of getting comfortable with it because you don't want to make the mistake of writing code that the tests wouldn't have forced you to write which is I think a common mistake that people make so for example like one thing that we could have done that I explicitly avoided was when we did like the create tweets table migration we know that we want the tweets to only be 141 characters right but I didn't set like a constraint on the database because there's no test telling us to do that yet but a lot of people might have done that so when you're just trying to like figure it out and get comfortable with the workflow I think it's a good idea to run them really often and kind of get used to the sorts of errors that you're getting once you get comfortable with it and you kind of can predict what the errors are gonna be all the time and you get comfortable with like making sure that you're not writing code that you shouldn't be writing yet you can totally skip running the test is off and as I am here and kind of writing more code at a time and you know when I'm actually doing this I'm not running the test quite as often as I am here but I do like to just kind of walk through it to kind of demonstrate like how useful the feedback can be and kind of how you can use that to drive out the implementation any other questions or do you want to dive into the next thing sure explanatory like the note note is empty and stuff like that oh are you just figuring out what that actually means through experience or are you looking in various places to figure out what that error is are you sure so the question was when you get errors from these acceptance tests that are not super clear like the node list is empty and it doesn't tell you like what line caused that where in your stack trace the problem is or whatever how do you kind of trace those down and figure out what the actual root cause of the problem is a lot of it does just come down to having seen them before and Trice trying to like figure it out when you're running to them so to me the biggest benefit of like the unit testing portion like that inner TDD loop is helping you get more useful feedback for a lot of things it doesn't help in that specific case would like the node crawler stuff but a lot of the time if you find yourself in a situation where it's like my tests are not giving me like useful information about failures anymore they're just kind of like blowing up and I don't know why a lot of time that can be a sign that you should jump down to the unit's s level and try and drive something out a little bit closer to the metal so that you get more more useful feedback in the the Dom crawler sort of case it's a little bit more based on just kind of experience and kind of just you know hacking through the weeds to figure it out but um yeah I think it's a useful thing to talk about this whole idea of you know using the unit tests to find the more useful errors a lot of time - okay I'm gonna dive into the next one because it's actually a longer example than this first one and I don't want to run out of time an hour and a half actually is not that long to build a bunch of features in an application but yeah so we'll just go and see what happens so let's kind of throw away everything that we just did and I'm gonna check out a branch that's time-traveling to the Future a little bit so we're a few weeks into building this application and now what we have this app called tweeter and we've got a user interface and you can post tweets like something about man lirik on is so awesome this year right Oh I didn't test that part anyways don't worry about it it's all good so more importantly related to the stuff that we're actually gonna go and work on here as you can like see how many people are following you or who you're following you know I can unfollow people I can like follow new people or whatever and we want to add this bit of functionality where anytime like you get a new follower or anytime someone follows someone we want to send the person who was followed a notification email saying hey you have a new follower sort of thing so let's drive that it with TDD now that we're kind of like a little deeper into the application and we're not just kind of doing the kind of getting stuff scaffold it out and kind of talk about a couple more interesting bits of interactions that you might need to do in your tests so I have a test in place already for following another user so you have this follow other users test which is basically what happens when we go and click that follow button right so we we create a follower we create a followed acting as the follower we make a request to the following endpoints storing this user name and then we just assert that we got redirected back to our following page showing us you know the list of people were following and we assert that the follower now follows the followed so let's write a test for making sure that we can send this notification email so the way that I would do this is um since it's kind of like a whole other part of this feature I would actually duplicate this test to start with even though the set up is gonna be very similar and just kind of make these assertions kind of their own thing so we can give this test a better name so instead of just having a test called test a user can follow another user that happens to have all these details about like mail and stuff like that I'd rather give it a separate name something like test users are sent notification you know when they are followed test names can be very long a lot of time but it's better for them to be descriptive than it is to try and aim for you know short testing so what is this gonna look like well we're gonna kind of do the same thing well like set up a follower and a followed let's let's sketch this out a little bit more so who could the follower be let's think maybe DHH ehh and who will have followed to be naturally Adam wagon my life's mission so acting as DHH we will try and follow me on tweeter the hottest new social network and then we got to make some assertions so kind of jumping out of the TDD workflow for a minute I want to kind of show you what it might look like if you implemented this code first and then tried to write the tests after and talk about how the results there differ from what it tends to look like when you write the tests first just to kind of convince you more that writing test verse is a good idea so if we head to our following controller we have this store method where they we get the user to follow by looking up that user by their username in the you know database we could use that find by username thing here but we didn't then we take the currently authenticated user we follow the user to follow and then we redirect back so if we wanted to send a notification email kind of the quickest dirtiest way to kind of get that going if we just want to just write the implementation would be something like just using the mail facade and sending a mail here right so it might do something like mail send and we got to pick a name for this template so maybe be like emails the new follower send some date along so maybe like who your new follower is and that's gonna be off user actually and then you got to do this like closure to kind of configure the message right and we're gonna need to probably use the user to follow font is a little big so things are wrapping a little funny hopefully it's still legible actually let's just get rid of the sidebar me a little home and then in here you'd want to do something like mail or message to should be like user to follow email then the subject should be something like I could type no something like you have a new follower something like that right so if we already had this code and we decided okay we want to go write tests for it after the fact what would it look like to do I think what people would naturally reach for is they would want to use like a mock right and a mock is a test double that lets you kind of set an expectation about what methods should be called and I see this happen a lot when people are writing tests after the fact they kind of look at their code and they see like okay I did this I did this I did this so I'm gonna make sure my test verifies that I do this and I do this and I do this so what does that actually look like in a test well you'd end up doing something like mail should receive because you can actually like chain these kind of mockery calls directly off of facades and levo which is kind of handy I'll make sure we import that mail facade here so it should receive what was the method called send and I'd have to specify what it should receive it with so should receive it with we decided it was gonna be emails to a new follower and that's some data which was like let's put this on separate lines so it's a little bit easier to to follow the data was gonna be like a follower which is the follower here and then you have to do like specify this closure somehow like saying mail sentence should receive this closure as the third parameter and people run into kind of problems here a lot in my experience because it's kind of really difficult to specify that you should receive this specific closure and what I see people often do is kind of resort to something like mockery type closure where they're just saying as long as it gets like a closure as the last parameter I'm gonna say everything is fine so if we run this test sue happens we get an error mail not found so make sure that we import the mail facade here what's our next air view emails done new follower not found so that's actually just happening because it's running this test first we're mocking it out in this test so I'm gonna kind of shut this test off just so we get some more useful information and it's green right so you might go ahead and think cool I've tested it everything's good I'm ready to move on to my next feature but to me this is like a super fragile test because what happens if like I sent it to the wrong person like what if I accidentally passed in you know off user or I guess we can just reach for that directly here right we can just do something like off user email and now if we run our tests I don't know if an empty use statement works they should all just die in a fire anyways so our tests are still green even though our code is wrong like we're sending a message to the wrong person so I don't really have a lot of confidence in that test right so what can happen then is you can get down this super dark grim path where you start doing something like okay well how can i specify what should get passed here one way to do that is to use this mockery helper called on which is kind of difficult to explain but the way it works is it takes a callback and that callback takes the argument that was actually passed into that position which in this case is a callback and now in this on method as long as we return true mockery is going to consider that argument to be a valid match for what you're expecting so if we did like return true here for example and just ran this test it's always gonna be green no matter what I do in in here right like this stuff doesn't you have to be there I can go back and run the tests and it's green it's fine so in here we can kind of do whatever is necessary to verify that callback is the callback that we wanted well how are you gonna do that there is a way that I will show you that will make you never ever want to use mocks this way ever again for the rest of your life so in trying to figure out how this is possible this is what I ended up landing on basically the callback that's getting passed here is this callback right and we need to pass a message into that callback and make sure that the message got the - method called with the right email and the subject called with the right subject so we can create a new mock so we can say mockery mock and then it's like message class I think make sure we import that illuminate mail message and then we can say something like well the message should receive - with and it should receive it with someone's email right so we're gonna have to like specify my email address here so email is gonna be hard I'm at example.com I was like that domain cost me a lot of money so chef received to with Adam at example.com and it should receive that once and then we have to make sure that we run the callback and pass the message in and then we still have to return true no matter what because we don't know that the callback was correct until mockery gets called in the teardown and verifies this mock so if we run this test now let's see what happens subject does not exist on the mock object so we need to also mock the subject method and say that it should receive you have a new follower once run the test in the green and now if we go back to the controller and we were to like passing the wrong email here right like nope now the tests actually do fail because you know mockery got called with the wrong thing so you might think okay yeah this is like solid this is like proving that my code works but it's like the ugliest I've ever see in my life and it's also like super fragile because you can chain these in actual implementation right like and if I run this test again it's going to break and you know why it broke because our Mach didn't return the message here and if we run this again now maybe it'll pass but now if we decided that we want to flip these around like maybe we called two before we call it subject put that arrow back in there and run the tests again now they're broken again so you have these like super fragile tests are super super super couple to your implementation you'd also imagine like what if I decided to like fire an event to like send the email instead of doing it this way well now this test is like totally invalidated like complete junk so never ever do that if you have code like that I've deleted so let's delete all this and talk about what it would look like to do this with test-driven development okay so if we want to do this with TDD instead of trying to like mock our exact implementation in our test we would think about like what is the outcome what are we actually trying to test well let's think what are we actually trying to test here maybe we want to do something like this we want to verify that the person received an email make sure the followed user received an email what else do we want to verify maybe make sure the email contains the name of the follower and maybe make sure the subject you know is you have a new follower sort of thing so when you're doing something in like a test-driven workflow this is kind of like how I would define my expectations up front I wouldn't be thinking make sure the male facade got called the send in this parameter in this parameter and then a mockery on call they took a call back that took a call back that created message that was a mock that defined these it really super gross so how can we do this though Rea like testing mail is kind of a difficult thing well you can use a different type of test double instead of a mock you can what's called a fake and a fake is like a real simple implementation you write of you know some interface or some other class something that can kind of be swapped out for it that mimics the behavior pretty closely but kind of does things in memory and kind of a light sort of way and often also exposes the ability to kind of use it for assertions and check things on it after the fact so an off time like walk through building went from scratch but I actually wrote a library for fake mail when I was working at Titan Co called mail thief which you can check out at Titan post lash mail thief on github and mail thief is like a fake mailer for laravel that lets you kind of do this sort of thing so the way that we would use mail thief here is the first thing that we would do is we would in our setup method tell mail thief to start intercepting all mail on the application so the way that you do that is by calling mail thief hijack and if we make sure you import this facade here now any mail that gets sent is going to go to mail thief instead of like trying to actually send it over to an SMTP server or anything and mail thief just stores these messages that were sent in an array in memory and lets you look at it after the fact so now to make sure that the user who is being followed received an email well what does that look like it ends up looking something like this this assert true mail thief has message for Adam at example.com and that's how you make sure that there was an email sent to that email address at some point in this test much cleaner in my opinion and the crazy mock stuff right now making sure that the email contains the name of the follower okay so how can we do that we could do something like this assert true mail thief and now we can say last message which gets us the last message that was sent to mail thief and since we are planning to only send one message here this is gonna work perfectly fine and then mail thief's message class is also a fake that lets us check things on the message so we can verify that the message contains DHH what a good day and now making sure that the subject is you have a new follower so we can just do something like this this assert equals you have a new follower mail thief last message o subject so we just get the subject of the last message and make sure it is what we expect it to be so let's just run this test because I probably made some mistake somewhere failed asserting that false is true so this is complaining that on line 44 mail thief does not have a message for Adam at example.com so using TDD let's kind of drive this out one step at a time so let's send a message to me so we'll call mail send we need to pass a template name here so let's kind of use that programming by wishful thinking approach so a new follower email some data but the tests haven't told us what data yet so let's leave that empty and then a callback that takes a message and I guess means you use the user to follow and then send a message to that person's email we run the tests get some new error there's no emails done new follower view so let's head over to our views folder here and we'll create an emails folder where we can keep our emails call it new follower blade PHP and we'll just leave it empty around the tests fell discerning that false is true on line 45 so what is the assertion there well the message doesn't contain the text DHH so that means that our first assertion passed so we did successfully send a message to Adam at example.com so how can we make sure that it contains the name of the follower well maybe you want to do something like follower username is now following you run the tests again undefined variable follower okay so we need to make sure that we make the follower variable available in the template so we do that by filling out this data array right so we can say the follower should be off user run the tests again now we're failing to assert that the subject of the email is what we expected you have a new follower so now all we have to do is go back to our controller and let's cover the sidebar again so it's easier to read message subject is you have a new follower on the tests and we're green how much better is that than the test that we had before with the crazy mocks and stuff to me it's like so much more expressive in terms of like saying okay make sure that a message was sent to this person make sure that it contains this text make sure that the subject was set to what we expect versus that crazy mock set up it's also like way more resilient to different types of changes so for example like you know we had talked about chaining these before right if we just chained them well guess what it does still pass we didn't have to change anything or if we call them in a different order or anything like that it's all it's all still gonna work because we're kind of using this kind of outcome style assertions about what we actually want to happen instead of just trying to mimic the code that we ended up writing in the controller in our tests after the fact now one other thing that you might want to do in situations like this just kind of a little tip I try to avoid like making like real specific exertion against text a lot of time you want to try and assert against the most stable thing possible so this is the sort of text that maybe someone on the design team might decide to change for whatever reason right like maybe they want to change it to like you have a new follower on Twitter you know maybe they decide that that's like a better subject if they change that implementation our test would fail even though like what we actually care about hasn't really changed in the system you know what I mean like the behavior isn't different now they just like tweaked a little bit of call so a lot of times I'll try and pick something like that I think is real stable so I might do something like assert contains new follower and make sure that the subject just contains the text new follower so it passes right now and if we were to go and say like you have a new follower on tweeter with two exclamation marks because it's so awesome it would still pass but if you know somehow we sent an email like I don't know what's an email that Twitter might send you something like you have a new DM or something right now our test is still gonna fail if we've actually somehow made a mistake and send like what you know it's basically the wrong email so I think that's kind of a useful thing to think about when you're writing tests like this like try and pick things to assert against that are as stable as possible so doing pretty good on time here which is awesome because it have some more cool stuff to show looking at our implementation for this a lot of people don't really like sending mail like through the facade and the controller a lot of time right like I kind of alluded to before kind of a common thing to do in these sorts of cases is like dispatch an event and something's listening for that event and it's gonna like send an email based on like that event being fired so let's work on like refactoring our implementation towards like an event based approach you're using test-driven development and just kind of like to see what happens sounds fun to me so the way that I would do this is since we kind of have this kind of super blackbox acceptance level test this test is still gonna pass even if we switch to an event based implementation right because if the event gets fired and the listener is listening for it and the listener sends the email well at the end of the day the mail is gonna be in the inbox and that's all we're serving against so we have a lot of freedom here to refactor towards different implementations and our test is not going to become like invalidated in any way so we can still use this test to kind of guide us as we move towards like an event kind of driven approach so if we head back to the controller let's just comment this out because we're gonna kind of need this code and I don't want to retype it again since we only got 25 minutes to go and you know could potentially take me more than 25 minutes to type four lines of code let's just kind of do this programming by wishful thinking thing again and see where we get so you might do something like this right event fire make sure we import that and we want to fire like a new new follower event or something right and let's you know kind of think about what our parameters might be well maybe it's gonna be like the the person who's doing the following and the person who's being followed could be the parameters so we'll pass in like Hoth's user and user to follow and then we can just kind of go back to our tests and kind of let these tell us what we need to do next so we get an error because there is no new follower class so we can do something like artisan make event new follower and then make sure to go back to the controller and import that event class and run the tests and now we get an error failed asserting that false is true so we're getting to that point where now we're in that kind of zone where our errors are very distant from the code that we need to write and it might be useful to talk about driving out like a unit test right so the reason that it's failing is because our inbox doesn't actually have a message for Adam at example.com and we have to think about like why does it not have a message for Adam at example.com what we fired the event but like nothing's listening for the events or handling the event right so let's work on a class that's like an event listener that can do this mailing stuff for us and drive out that event listener with TDD so in our unit folder I'm gonna create a new folder called listeners I like to have my unit folder kind of mimic that hap structure usually it makes it easier to find things and we'll create a new file will call this listener something like email new follower notification test dot PHP and let's just copy our follow other users test for now because it's gonna have some similar sort of stuff going on so this will be email new follower and notification tests let's kind of just delete anything that we know for sure we're not gonna really need and the tests name can kind of still be the same or yeah maybe test it sends a notification email to the user being followed okay so our setup can be similar will have a follower and a followed this actor SEP is gonna be a little bit different though right we're not gonna like hit an endpoint anymore we're gonna just exercise this listener that doesn't exist yet so let's create that listener so we'll say the listener is gonna be a new email new follower notification listener and then we want to do something like a listener handle and we're going to handle a new new follower events which is gonna have the follower and the followed and now aside from that I think like what we're actually trying to verify at the end of the day is the same or if we want to make sure that that person got that email so let's just run this test and see what happens no such stable users so we got to go back and make sure that we have our database migrations setup right run the test again class email new follower notification not found so let's generate that listener so we can go Hardison make listener email new follower notification listener and then we have to specify the event that it's meant to accept on the command line which in this case is a new follower event we head over to our listeners folder now you can see that we have an email new follower notification class in place just kind of scaffold it out for us so this will get us past point of the class not existing now maybe non so we gotta go and import it now that it exists do we have the new follower imported as well might as well do that run this test again failed asserting that false is true so we're kind of back to that same error where it's failing to assert that the message was sent so how can we kind of like make that pass so this is where I'm going to go and steal this code that we wrote in this controller and move that to our listener so for now let's just leave it exactly the same even though we know it's totally wrong and run the test and just see if we get some more useful errors there's no off class in the Lister's namespace well we actually don't need to worry about authentication at this point right because we just we explicitly have the follower and the following which is kind of nice because we're working at this kind of more constrained unit level where it kind of specify the parameters and data with a little bit more precision so rather than passing an auth user as the follower here we actually wanna get the follower off the event so we can say the follower from the event run the tests again undefined property there's no follower property on the new follower event so we'll go to the new follower we'll inject a follower make it public so that we can access that run the tests again undefined variable user to follow okay so here we're trying to like use this user follow thing but it doesn't actually exist instead we want to use the event and get the followed from the event run the tests again undefined property followed so now the new follower event is missing the followed user so again we can inject that make it public run the tests in our tester green that's pretty cool now a lot of people are content to use like facades and stuff and the controller layer but when you have like a constructor that's available here and you're in like a listener class a lot of people feel like well I might as well just inject it right it's just as easy so let's let's do a little bit of work to actually like inject mailer instead of doing it this way so we can head to our tests where we create it here we can just pass in male thief's instance it's kind of like the singleton male thief instance head back to our controller kind of inject a mailer and then down here we can use this mailer sentence and if we run these tests hopefully we're still green and we're still green so now we've driven out this kind of event listener with TDD and if you're not paying close enough attention you might think sweet we're done everything's good but if we go back to our follow other users test and run it we get an error why are we not passing this test well this is kind of where the real value of acceptance tests start to like show themselves in my opinion and what's happened here is we've forgotten to wire up our event to our event listener so even though all our unit tests are passing our acceptance tests it actually exercises the whole system and is making sure things are wired up it's failing and you know telling us hey you should investigate this and it's not always going to give you a clear error as to why but at least it fails so you don't deploy something that's broke into production so if we head over to our event service provider we can point the new follower events make should we import that at the email new follow our notification listener and run our test again still getting an error I'm resolved with dependency mailer got it got out of type int so if we type int the mailer contract here run our tests now we're green and everything's actually working so that's pretty cool now continuing if we go and look at our follow other users acceptance test versus our email and the follower notification test they kinda are real similar right and you might make the argument that we have redundant coverage here in the sense that we have our listener that's kind of verifying that like yeah when the listener gets called it definitely sends the email and everything's kind of good but also our acceptance test is trying to verify the exact same thing and in a lot of cases what you might choose to do and which I think we should walk over and talk about is instead of verifying that the mail got sent especially now that you've kind of like used it as a black box test got everything kind of working a lot of time you might decide okay well now I think all I want to verify is that the event gets fired and let you know the event listener test worried about making sure that the event listener is actually doing what it's supposed to do so laravel supports the ability to kind of like make some assertions about events that got fired and stuff out of the box and we can do that by doing something like this so if we if we tweak this acceptance test get rid of these assertions at the top of the test we can do something instead like actually it probably needs to be down here I know I can actually go up there for now so we can do like this expects events this is gonna say well what event should be fired and you could just do new follower class here and if we run this we get an error these expected events were not fired so we're gonna import that class probably yeah and now we're green all right so you might think that this is enough I don't and the reason for that is this is not verifying enough to me it's not verifying that we're necessarily firing the event correctly and I'll show you an example of how this could be problematic imagine that in the controller here you kind of made the mistake of getting these two parameters mixed up so you passed like the user to follow as the first parameter and the follower is the second parameter well this test passes because it did get that event fired the listener test passes because when we test the list and we've tested them in the right order but the application doesn't actually work properly now so I don't think this is quite enough in terms of verifying that like the event was sent the way we expected so how can we go about doing it a little bit differently in a way that's a little bit more robust so this is a situation where I think using a mock is like a reasonable thing to do so we can do something like this we could do something like event and make sure we import that facade should receive fire and it should receive fire with a new new follower events that takes the follower and the followed right and should receive that once let's just run these tests and kind of see what happens so we get an error complaining about mockery did not receive or sorry there was no matching handler for the event that we actually fired and it's gonna be confusing at first but the reason this is happening is because in our controller we're like mooing up a new follower event here it's not like injected or anything and we can't inject it like it doesn't make sense to inject it and then the acceptance test when mooing up a new follower event as well and these are like in like a shallow way they're kind of the same right they're both the same class they both took the same parameters but one of them was nude up over here and one of them was nude up over here so when mockery tries to compare them and make sure that like it was sent the right event it's saying no you told me to expect this new follower event and I received this new follower event instead it's like an object ID like in memory sort of clashing problem right so to get around this we can use like that mockery on thing that I talked about before so you can say like mockery on that takes an events and then you could say something like return the events follower equals the follower and the event followed equals the followed and if we run this hopefully we get some some new insights so undefined variable follower so we've got to use the dreaded use statement run the test again and now it's still complaining about not getting something that matches what was expected and this is actually just a symptom of like the exact same problem that we were just talking about with the new followers events in this case like it's not the same followed class or our instance story so you end up having to do something like this event follower ID equals follower ID an event followed ID equals followed ID and run the test again and now check this out now we get an error that like this event was fired kernel dot handled as an event that laravel fired in the core but we didn't have like anything programmed in mockery to actually like listen for that event so mockery is like hey I received an event that I wasn't expecting your test is broken so to get around that you have to do something like this event should ignore missing which basically says swallow any methods that get called on you that you're not programmed to be able to respond to properly if we run that now it's green okay so that we made progress now one thing that I don't really like about using mocks and this is not really specifically to like setting expectations per se but about mocks specifically is that when you're setting an expected on a mock you have to do it at this part of the of the test right I like to think of my tests like organized in a certain way traditionally it's called like the arranged phase the act phase and then the assert phase right so you're kind of setting up the test you're kind of doing whatever it is that you want to test and then you want to make some assertions to make sure that what you expected to happen actually happen in this case we sort of have an arranged step this is kind of like an arranged slash assert step and then we have like an active step and I don't really like that I have some tests that are kind of organized one way and some tests are organized another way and this gets particularly gross if say like this is something I actually do like would do pretty often in a case like this I might now collapse this down into one test because it's not super specific about sending a notification email with a ton of assertions instead it's just verifying that the event got fired and because acceptance tests are most often a lot slower than unit tests it's often beneficial to figure out ways to kind of reduce the amount of them that you have and somehow combine assertions in a lot of cases to kind of speed up your tests we to kind of you know make your workflow feel a little bit smoother so if we had like if we want to combine these right like maybe we use this arrange step we use that act step we copy this up here and set that up here we're gonna have to change this following thing so we'll use this one and then we can delete this tests and make sure that this test is turned on again yeah it still turned on now if we run it it's still green so we kind of did everything right but look how sort of like unorganized and sloppy this test kind of feels now right like we're doing some arrange we're doing like some mock programming as well as like telling the event to ignore certain things we do some acting and then we do some assertions now these are assertions but like they don't live with the other assertions and I don't know I don't really like that so there's an alternative to using mocks and it's a feature that mockery supports it's kind of like a bit of a hidden feature that's not really commented called spies and now a spy does the same job as a mock in terms of letting you set an expectation and make sure that you know certain methods were called in certain ways but a spy lets you do it at the end of a test instead of at the beginning of a test so check this out if we cut this and move this down here and at the top of our test in our kind of arrange phase say something like event spy so laravel supports this as of five three for sure it might have got backported into five two as well so you can call the spy method directly like on a facade and instead of things should receive fire with this stuff we can say should have received fire and if we run this test if we didn't make any mistakes everything is still green and the other nice benefit of this is that because spies are always defined at the end by default they're programmed to ignore missing calls so I don't even have to say this anymore this can actually go away and I can run this test and everything is green and if we were to go to our controller and you know we made a mistake and we're firing the events anymore you know it's gonna fail because fire was not called so we know that everything's still kind of working the way that we expect it to work and now we have a test that's kind of organized in kind of a more consistent way and yeah has a little bit less stuff that you have to specify and we've kind of removed some of that test duplication one other thing I would like to talk about quickly that I meant to talk about earlier but it's a long talk with lots of stuff so it's kind of hard to remember everything is that here in our event or our email a new follower notification test we're creating these users and sticking them in the database right and if we look at like how long this test takes it's like a hundred and thirteen milliseconds 117 milliseconds but we actually don't need these users to be stored in the database for this test because all we're doing is like reading the user name the user name and the email and making sure that we fire an email we're not actually like saving anything we're not actually like fetching relationships or anything like that so in this case we can actually switch create to make now if we run the test right now it's not going to really be considerably different but now we can remove this database migration stuff and this test can run without a database and now it's running at 78 milliseconds 70 milliseconds sixty-four milliseconds so you can imagine if you can make these sorts of little tweaks and lots of different places in your application that it's gonna add up pretty quickly over time and speed up your test suite now there's a cost to it in the sense that you're coupled to the fact that this can't depend on the database now and if the implementation changes in a way that does depend on the database well then all of a sudden the test is gonna break but it's just a trade-off that sometimes you have to make for for test speed and stuff like that so anyways 10:23 supposed to be done at 10:30 managed to get through I think everything I want to show you people and yeah so that's kind of my test-driven development workflow and laravel that's kind of how I build stuff out hopefully you learned something about kind of a good way to kind of do this stuff and you know took some stuff away from kind of the decision making process that goes into deciding like when I should create a unit test when I should use the database when I should them when I show to use a mock when I should not use a mock how to use a spy how to use a fake instead of a mock stuff like that we talked about a lot of different stuff here so hopefully everyone was able to pick up a couple new things and learn some new things and if you have any questions or you want to talk about this stuff more I'll be around come find me I don't think we have a ton of time for questions here but just come find me we can talk about it and yeah that's all I got for you so hopefully you guys enjoyed that now one other thing actually I said I had something to like announce here and that is that's I'm actually working on a new project test-driven laravel like a full video course where i'm gonna be building an entire application with TDD and screencasting the whole thing and recording it and you can sign up and to learn more about it at test-driven laravel comm and i'm hoping to release it this fall so if you enjoyed this and learn some stuff check that out and subscribe and I'll keep you updated and let you know when it's ready and yeah thanks everyone I'm Adam wagon check out my podcast something [Applause]
Info
Channel: Adam Wathan
Views: 10,864
Rating: 4.9065418 out of 5
Keywords:
Id: MdApmmK71WM
Channel Id: undefined
Length: 80min 13sec (4813 seconds)
Published: Thu Jan 03 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.