Test Driven Laravel
Lesson 4 of 6 · Test Driven Laravel – e04 – Book Checkout & Book Checkin Flow With TDD · Education
Test Driven Laravel – e04 – Book Checkout & Book Checkin Flow With TDD
and we are back for episode 4 of our library project that we've been working on and first and foremost I want to run all of the tests and I want to make sure
that we are still at green remember we always start at green and we want to end at green ok so now that we have a book and author the very basic thing that a library needs to do is it needs to be
able to check out books so we have this idea of a check out and of course a check out pertains to a particular book and a user and on top of that we need a timestamp of when the book got checked
out and we'll need a timestamp when the book gets returned now based around parameters we can determine whether that book is late or not or whether that book may even be lost we may have to handle a
case where the book never is returned so let's go ahead and start out with the most basic reservation that we can have so for that let's make a test PHP artisan make a test and we'll call it
book reservations test let's go back to phpstorm and take a look at book reservations tests alright so as always we'll go ahead and get rid of this test case and start with a fresh test so what
is the very basic thing that we need to do well we need to make sure that whenever a book is checked out it actually gets the correct timestamp so we'll start with that obviously we need
to build a whole entire world around this first test because there is no model there is no table there's nothing for us just yet so let's start with maybe an experimental test we may end up
changing this a little bit but we'll say a book can be checked out checked out very vague I know but again we got to start somewhere and some of these tests sometimes you'll find you
got to start somewhere and then you make your way into what ends up being the actual test you might end up dividing one test into two or three different tests but at first you
can write everything just in one test and figure out where you want to go remember we're not even sure where we're going with this just yet no real agenda here we just want to make
the code feel good as we're writing it so what I'm thinking is this if I have a book of some sort I want to be able to call maybe a checkout method on it and maybe pass in the user so that would
be one option so we have a book and then we can check it out for this user alternatively of course we can have a user and we can check out the book so up to you what approach you'd like to take
but obviously ultimately we'll need to use one of these we either have a book and we check out the user or we'll have a user and we'll check out the book now we can simplify this a little bit
because really we could use the authenticated user to do it but if we did and this is why I would encourage you not to use that instead of grabbing the authenticated user sometimes we may
have a user which is maybe a librarian who is able to check out books for another user so if we lock that in to just the authenticated user they will never be able to have a librarian check
out a book for somebody else instead of like a self checkout so we'll keep that in mind we do know that that's a functionality we're going to need so I'm gonna go with this approach I'm gonna
say if I have a book I want to check it out for that user that makes sense to me so let's get rid of this and that will also help you not have a user god object it's very common for the user model to
just get everything dumped into it because really a user can do anything in your application so we can always pivot everything around the user but get in the habit of pivoting around something
else that's not the user it will help you use a model stay nice lean and clean so how do we get a book well let's go ahead and whip up a book using a factory so say book class go ahead and create
one for us and of course we did import book up here at the top now we do know off the bat we need to be able to refresh our database as we done with all of our tests we don't need faker just
now so this book that we created we need to save to a variable and that's the variable that we're going to use to do that now in the case of the user of course we do need a user so let's also
create a factory for a user and go ahead and persist that to the database create let me go so if we check that out so if we took our book and we checked it out for that user what do we expect to
see well first and foremost I expect to see something in some sort of table so let's assert a count of one in maybe the reservations table how about that if we call that table reservation so in the
reservation excuse me not class all if we dumped all of our reservations than I expect to have one now furthermore if I actually grabbed that first record then I expect maybe the user ID to belong to
the users ID so let's go ahead and grab the reservation doesn't exist yet we'll grab the first one and then let's grab the user underscore ID in the same token I expect the book ID to also belong to
that book to this book and this user and we need to assert something on the checked out time let's the assert equals now again if we grab the reservation first and then let's grab the checked
out at none of this exists obviously this whole code does not exist now one thing here the way that we wrote this test is this a feature test or is this a unit test this is really a unit
test right because we are actually grabbing the particulars checking it out so this would not be considered a feature test let me show you the difference if we go back here and take a
look at for example the author management test this test actually hits a particular endpoint with some data and we're basically simulating the same exact thing that a user would do if they
went through our browser interactor with our website sent in some data and then our application did what it had to do that's a feature test this here is really a unit test because we are at no
point doing anything that a user could do directly and that's what makes it a unit test I will want to actually move the book reservation test into my units directory and all we'll need to do here
is change the name states from feature to units now at first I thought that this was going to be a feature test and that's why I put it in the features directory but sometimes the way the test
works itself out it's not we will need to write a feature test because we still need to hit a particular end point we need to have that under test that a user can hit an end point with some data we
know it's going to either be the book or the user and with that data we need to make sure that our application does the correct thing so we'll need to go back and do a feature test for this but for
now this will do for this test let's go ahead and run it and let's see what we have right off the bat unable to locate factory for a book let's go ahead and make one now PHP artisan make factory
and before I do that I what do you want to show you that you can add the model name right on it so let's run PHP artisan help make factory and you do see here you have this – M so you can pass
in the model of the factory that you are creating so PHP artisan make a factory book factory – M for the model and of course it's gonna be a book whoops looks like I misspelled artisan artisan there
we go alright phpstorm book factory there we go so now you see that we already have book in here that's kind of nice let's check out the migration for a book so we know what
fields we have so we have a title and we have an author ID so author ID we could either pass in a 1 or we can create one on the fly I'm gonna show you two different ways of doing this back to my
book Factory the first thing is a title and as you remember we do have faker so we could say something like faker give me a sentence how about that sentence and then for author ID well we could do
two things yes we could pass just one but if we ever needed this to actually belong to a book this would not work so another trick that we could do here is one of two things we can either run a
factory right here for an author we could do that and we could create that and then we would actually need the ID of it but here's a trick the second thing you can actually do is instead of
creating it on the fly if you remove that and just pass in a factory laravel is actually gonna do a pretty nifty thing for us and what it's going to do is this if no author ID is passed in
meaning when we run this right here when we run the book creates we could pass in an array and say author ID and pass it one two three so if we do that then letter vu is not going to create an
author force now if we leave it the way it was like this without actually passing in an author explicitly then what it's going to do is actually create one force so you may be thinking well
why does that make a difference well that makes a difference because if we create this right here then every single time we create a book an author is going to get created even if you manually
overrode the author ID an author still gets created in our database so it's nice that label knows not to do that if you don't pass in the create so all we need to do is just pass in a factory
letter well knows what to do with it so it's pretty cool all right so we have that now let's run that test again okay so now we are getting author not found in the factory let's go back here this
right here needs to be inputted if you look up here of course we do have book but we don't have author so let's go ahead and create that now book author okay let's run that test again
okay so we're onto the next thing so unable to locate a factory for an author of course we didn't create that yet so PHP artisan make a factory for author factory and of course our model
is author great back to phpstorm author Factory and let's check out the migration to see what we need to do so create authors table we need a name and we need a date of birth but it is
nullable so we'll go ahead and pass one in anyway so back to author factory name will use faker again and we'll say give me a name and then for date of birth let's use carbon so
we'll say now let's do some sub years and let's just go back ten years that will basically default to today's date ten years ago obviously date of birth that's just a test dummy data but we do
want to make sure that we do that now it's underlying it because this should be sub years let me go sub years ten okay back to the test all right next the method checkout
doesn't exist on the book of course we know that all right let me close everything up let's open up our book model and let's create that now so right under here path let's do public checkout
okay see here we have now it says reservation not found so remember a reservation is a particular reservation and it's a model we haven't created that just yet yet we did reference it in our
test if you remember back here in our book reservations test we have this reservation it's a model it doesn't exist so we need to create that now PHP artisan make model reservation and of
course we're going to need a migration for that so we'll go ahead and create one now now we can import reservation so we're going to import that class up here at the top as app reservation and now I
think we'll be okay yep we've moved on to the next error the next error is that it failed to assert that there was anything in that database at all and that makes total
sense because well we're not quite doing anything when we actually hit this check out so let's go ahead and we know we need to accept a user so let's go back here to book and let's accept that user
now yes you could actually require that to be a user if you wanted to so it depends on how strict you want to be so what this will do in PHP is it would make sure that this object of user is
always of type user this will help you basically lock down your application so that you can't accidentally pass in a book to your checkout for example or something like that it's up to you how
strict you want to be we're going to keep it simple we're just going to keep it open we're gonna hope that our tests will protect us from doing something like
that and other times there's a lot to be said about just keeping it simple until it needs to be more complicated keep your code as simple as possible make it work and when the time comes if required
you can go ahead and make that refactor I doubt you're gonna run into that issue though because if your tests are green or passing on our testing your code and you've got good coverage there's no
reason why you should be passing a book to your checkout method so that should not be a concern so once we have our book and we have our checkout what do we expect here well at some point we're
gonna have to create a reservation so we'll have to somehow create a reservation and we know that the reservation is gonna need a checked out at and checked out at is gonna be now we
know that what else is it gonna need well it's gonna need a user ID and we'll set that equal to user ID and that's about it but it does need a book ID so what we could do of course as we've done
before is we can create a relationship for this so instead of calling it on the reservation directly why don't we actually use this reservations and then on that let's go ahead and call create
now we do need parentheses on this so this reservations reservations is referring to a relationship that doesn't exist yet and we are creating on that relationship so let's run our test and I
bet you were going to get an error about that and sure enough we say well there is no reservations in our book okay let's go ahead and create that now so right down here we'll say a new public
function reservations and what is this what kind of relationship is a book to a reservation well a book can have many reservations and a reservation belongs to a book it
also belongs to a user so we'll have to create that as well so let's return this has many what does it have many of has many reservations class and that's it so now this is a relationship where this
has many reservations and this being of course our book because we are inside our model let's go back to this test run it again and now we get a mass assignment
exception saying user ID is not fillable in reservations we know that so let's go ahead and go back to phpstorm and in our reservation model as we've done before let's turn off this fillable
protection that we have by setting guarded array equal to an empty array so this basically turns off mass assignment protection we know that all right so up next of course this database doesn't
contain a column by user ID so let's go back to the create reservations table and let's start with that user ID so table unsigned big integer for user ID let's run our test again so now we don't
have a checked out at let's go ahead and copy that let's create a new one for table timestamp for checked out at now we don't have a book ID let's copy that okay put that right here okay
and there we go we are green so we went through all the steps and we got through to creating a reservation so now we know that we have the logic to prove that a book can have a reservation and if we
check out a book then of course we do in fact create a reservation in our database awesome all right how about this let's create the inverse of that so let's pretend that the book got checked
out but then it got checked back in so now we need to know that it got checked in at is not nullable so let's create a new test and say a book can be returned how about that obviously if it could be
checked out then it needs to be returned let's cop some of this code right here so we'll still have a user we'll still have a book then we'll say book may be returned or we can keep it in the same
say check-in instead I do like that let's go ahead and check in the book for that user do we need a user not really I'm not really either concerned and who returns the book as much
is the fact that I just want the book to be returned so if the book is returned so yes our reservation should still only contain one and of course remember a reservation will have a checked out time
but then we're going to add this new checked in time so then we really don't need to assert that the book ID and the user ID are still there but I'm gonna do it anyway just in terms to make sure
that our code doesn't accidentally override the book or the user ID and then I want to make sure that checked in at is right now okay but here's the thing this book was never checked out so
this should almost throw an exception and we'll get to that in just a second we'll write another test for that because if a book is not checked out then there should be an exception right
so we'll run another test for that in just a second but we still need to check out this book before we can check it in we need to check it out so we'll create a book we'll create a user then we'll
take that book and check out that book by that user then we'll take our book again and make sure that we check that in then we're going to make four assertions we're going to make sure that
we still only have one reservation not two there should only be one and then we need to make sure that our user ID still matches with our reservation and our book ID still matches with our
reservation and then finally that we have a check-in at time and it's just gonna be a time step of when the book got returned great let's go ahead and run this test and see how we can make
that work so first and foremost check-in that doesn't exist in the book let's go to book and right underneath checkout let's add check-in okay now of course we're gonna get something about nothing
matching because null does not match the expected object that's line 41 let's go back to our test this all passed right but this did not pass because checked in that is actually not even set if you
wanted to get a particular error on that we could say assert not no on that particular field we could take this first and we make sure that that's not no and I bet
you now we're gonna get something different and we do so we failed a certain that no is not no of course not is no so back here what do we need to do here so the first thing we need to do is
actually match up our book reservation so it turns out we are gonna need the user ID after all because we need to be able to match up to any reservations that are in our database let's actually
accept the user and pass in the user through the check-in method so now that we have that let's go in here and say all right so this reservations go ahead and give me one where the user ID
matches the user ID that was passed in but we also need to make sure that it's not know meaning it is checked out and how do we check if a book is checked out well if it's checked out it means that
the checked out time is not know so if it's not no then the book is checked out checked out at is not no and then go ahead and get me the first one we should probably also check that the check-in
time is no so we'll also check that so here it is so we're grabbing our reservations but then we're going to scope it to just the ones where the user ID matches the user ID then we're gonna
make sure that checked out at is not no meaning the book was checked out but at the same time the checked in at is no and then we're gonna grab the first one now another little test that we need to
make sure is that a user can check out a book twice so a user can check out a book twice it's another little test we need to run because we need to make sure that our user if they check out a book
and check it back in that they're able to check it out again and check it back in without interfering with each other so these are the little cases that you're going to see that we're going to
add on tests for to make sure that our code is doing everything that it's supposed to be doing so now this is going to give us our reservation so let's go ahead and save that and then on
our reservation what I want to do is the following so reservation I want to update what do I want to update I want to update the checked in at and set that
equal to now because of course we are checking in this book okay let's run our test so now there's an update on no so we were not able to fetch this correctly and the reason for that is because this
checked in that doesn't exist let's go ahead and add that to the create reservations table is that another one for checked in at now check then that needs to be nullable and the reason for
that is because remember whenever you check out a book of course the checked in that is still gonna be no otherwise you wouldn't know exactly when it's gonna get checked in at so it definitely
needs to be know so we're not test again and we're back to green awesome if we run our entire suite we're still at green so we're doing great let's tackle those two edge cases that
we just talked about let's start with this one so a user can check out a book twice alright let's turn that into a test a user can check out a book twice let's go ahead and steal some of the
setup from up here you know what let's steal everything alright so we're gonna go ahead and create a book then we're gonna create a user then we're gonna check out that book then we're gonna
check it back in but then I'm gonna check it out again so that's the action that we are checking so if we did this and then we checked it out and then we checked it in and then we checked it out
again meaning the user returned the book obviously we're doing this in a matter of seconds but a person checks out a book takes it for a week brings it back three months later they come back and
try to check out the same book we still want to be able to do that so at this point we assert that we have how many reservations well at this point we should have had two reservations because
this is one and then this should have started a brand new reservation now of course the reservation we can no longer use first because of course the first one is going to be referring to this
initial reservation what we really need is to just go ahead and switch us over to find and let's find it with the ID of two meaning the second time that the
reservation got created we know that the first time it got created everything should be okay so at this stage we'll make sure that our user ID on that reservation is correct we'll make sure
that the book ID on that book is correct and then at that stage we do need to check that it is no add the check did not write because at this stage it is no because we have not checked it back in
just yet and of course we want to make sure that's not the check-in at but the checked out at is equal to now now we want to test a little bit further then we can check in the book and then at
this stage we could do a couple more checks so we're still going to grab a lot of this code here so at this stage we still expect to have two reservations in our database we still expect the user
and book ID to be correct but we expect not know on the checked ena and we expect to check in at to also be filled out so this is a pretty long test but I think it proves what it needs to do so
let me run this test and let's see what we get we get green so we are actually good to go we are still making sure that that is happening so now check this out how can we break this test if we go to
the book model and we actually did not have this line right here if I got rid of that line I think that will break that test and it does so that line that we add it right there is the line that
requires that we are when we are fetching the correct reservation we need to make sure that the checked in at is null otherwise it may be an old reservation so if we run that test with
the line back in then we do get green and if we run the full suite and we are good to go okay so let's go back here and let's tackle this other edge case so if it's not checked out and there should
be an exception so let's turn that into a test if not checked out exception is thrown all right so let's copy some of the setup over here I'm gonna create a book and
we're gonna create that and if we try to check in that book without checking it out then we do expect an exception so to expect an exception we can say this expect an exception and for now let's go
ahead and just expect a regular exception nothing fancy we may at some point create our own exception saying something like book is not checked out exception for now let's just throw a
regular exception no problems there all right let's go ahead and do that now let's run the test and of course we blow up because we're getting called to a member function update on No
so what is actually happening and I'll tell you what is actually happening let's go ahead and dine dump reservation right here let's check out what happens so we get null so that's just it we are
still trying to fetch a reservation because we're checked in but turns out it's no because we never checked that out so we never created a reservation for this book and this user so this is
null so we could do a quick check here many ways we can tackle this as always but we could say if is no reservation then let's go ahead and throw a new exception like I said we're just
tackling a regular run-of-the-mill exception nothing crazy let's run that test now and now we're back to green run our full sweep we're still at green does that make sense
again we don't want somebody to be able to check in a book if they've never checked it out that is just not normal operation for a library that's an exception kind of thing so I tried to
check in a book and it was never checked out and clearly something went wrong along the way during the checkout process so we'll throw an exception for that so this is the unit test for this
entire thing we do need to create some end points to make this logic be activated through the browser so in the next episode let's shift our focus into a feature test as opposed to a unit test
so we can create the necessary end points to check out and check in books so go and apply these changes to your project and then when you're ready let's move on
to that feature test
It’s time for us to drive out the checkout and checkin functionality for our app. Let’s start with the flow of checking out a book and then the flow for checking one out. Let’s also handle some edge cases along the way using test driven development to guide us.
For the best experience, follow along in our interactive school at https://www.coderstape.com
DigitalOcean Referral
https://m.do.co/c/7dce5364ef4d
Resources
Course Source Code
https://github.com/coderstape/library
Hit us up on Twitter with any questions or comments @codertape (https://twitter.com/CodersTape)
About This Course
We are tackling a brand new series to showcase a real-world workflow that you may adapt to your liking. We will be building a library management software for our local library that will include the ability to checkout books, reserve books and much more. We will be doing this through the TDD methodology, come along as we dissect Test Driven Laravel Development.