Drupal 8 TDD, by example: Writing a Form, part 1

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hi guys so this is going to be a multi-part tutorial to show you how you can use test-driven development practices in Drupal 8 I will focus on writing a form this form will have a few particular aspects like a file upload with a custom file validator the form will be different based on user permissions and then there will be some basic validation as you would expect to see in any form and then upon submission the forum will create some entities so the goal is really to use TDD so I'm gonna stick exclusively to unit tests wherever possible but obviously for the entity writing and upon submission we will need a kernel test but two goal is really to show you that you can test Drupal forms without ever relying on a functional test so with all this let's begin the first thing we're going to do is set up our testing environment I'm gonna use the Drupal composer project for Drupal which is a tax dead and that's called a d8 TD so if you've never used the Drupal composer project really recommend you look into it it's really really good so I'm just gonna take a minute oops so now that this is set up so basically if you don't know it it puts the the bender and some helper scripts outside the web routes and Drupal itself is inside web so what I'm gonna do here is I'm gonna prepare a custom module which I'm gonna call form validation here we go and I'm gonna open it in my IDE so here we are so the first thing we want to do is of course flesh out our module suits the first thing is to create an info llamó file I'm a terrible diaper obviously try to imagine so before I begin I want to show you what I have in mind for this forum so i'm typically typical tdd you would first start writing your tests but i find it easier sometimes to at least get the basic structure flashtab it helps me to focus then on actually writing the logic so this is going to be our forum we're gonna use first input form stands for peace so first thing we want is we need the get four ninety function will show me for knowledge in words like you just inherit the documentation and the other thing we need it's built for and then in later videos I will show gonna put it here already back that leash go back and obviously the submission Banco like cut it so um an art form will have a file upload which will be a CSV file upload the type is gonna be managed file for reasons that will become apparent later and the idea is that the structure is as follows the first column contains the book title and all the other columns contain the authors so you need a minimum of two columns but you can have as many as you want and what we want to do is that we want to we want to call different of validation callbacks to validate the upload so the first one we want to call is the Drupal core validate extensions the file validate extensions callback and we want to see that the extension is a CSV and then we want to call our own callback which I'm gonna call form validation of the day CSV keep it nice simple and we don't need any other parameters except of course the file gets passed anyway next while I'm at it I'm gonna finish building my form so I'm gonna put it but did I actually I'm going to test this but that wait it's already done so this would be a submit button the value of course localized and the button type of course be the primary button and reversion okay so as you see now we have our very simple form and the first thing we want to do is we want to write logic for the file validation so the way this works we need to define this as a function and the location where you define this is indeed ng modules dot add your file relation module so file validation callback and here we are going to use the file interface and a file validation callback should return an array of errors so if the list is empty that means that there are no errors this nation shoes if any so here is where we're actually gonna start our TDD so we want to test this function plus it several different files and make sure that the logic works the way we want to do this of course is by creating source unit some unit tests I'm going to create a new HP class which I'm gonna call yes the validation test I'm gonna put the namespace afterwards because phpstorm tends to get confused it stands the unit test case we financial unit test and that's better let's put it in the correct namespace which will be Drupal for validation tests units here we go so here we have our test case just validation so basically we want to have some test file a test CSV file that we will pass to our function and we want to check that we get to correct there so let me create a fixture pictures let me start by creating a file with let's say incorrect an incorrect format so I'm gonna call it books in corrected format CSV so title officer so we will only accept a CSV that actually formatted with a comma so here I'm gonna use this semicolon so this shouldn't work to Kill a Mockingbird by Harper let's bring magic programmer by Andy Hunt and Dave Thomas here we go so so this is another example valid CSV but we will not accept it and let's check ours so we want to this assert equals we expect an error message how shall we write this let's say the CSV format is incorrect use commas mean which is on your line unbeatable and here we want you court function and here we go now this obviously is not gonna work because we know we need to pass a file here but in typical first Urban development's let's actually use it so we're gonna use PHP unit the peach per unit file is in webcore and this is a little trick you can use as you pass the path to where the tests are located this is a bit faster so you could use like filter or group but if you use filtering group pH pin it will still have to parse all existing existing unit tests if you do it like this the execution will be much faster so if we run this will fail boom call to undefined function for validation test unit for validation CSV so what's the problem here of course we have not loaded our formulation module file so how come around it something up function set up of course we need to keep the parent set up the unit test you always want to parent set up at the top so before you start doing stuff let's require once so this is not clean display but bear with me so we are in so we are in unit AK or it's worse in tests teacher units is very helpful PHP storm is very helpful to it we know that this is correct so now we require boom okay so it complains we're not passing enough arguments to our forum validate C's week which is indeed correct this is obviously wrong so how can we pass this so the easiest way I guess we could tackle this is to actually use a testable instead of creating an actual file because if you want to use an actual file we're gonna use needs you have a kernel test and that's not what we want to use a unit test so let's create a file this yet mark and we use the file interface and expects so what we will do is that we will use the get file you URI method to actually load the file so will this return value will return the values so let's move up to go to fixtures and then we use books incorrect come on okay 400 title in current form it should be format suphp strong hopefully change the dirt as well so let's pass this file now try validation you see that now eh pissed on about complaining anymore so this should at least not give us there boom it's working so let's give this a new error and that's per taste now you might notice what I did here I'm using T because this function is supposed to return a localized version of the error message and now you will see that we'll have we're gonna run into an issue so if I run this cold undefined function T now why is it saying this it's saying this because in a unit test you do not have access to any of the Drupal AP is true you can load all the classes and everything but the only reason you can do that is because you you are using the composer o to look dumb but in fact there's nothing of Drupal there which is why you cannot use function T now how do we get around this if you want a quick and dirty version you use a kernel test or god forbid a functional test but we want to use a JUnit test so how can we get around this limitation we could try try and make it test double for T but it's really fragile and it's best to avoid it so one thing we can do is that we can actually use a class if we use a class the class can inherit can use the string translation trait as it should and if we use the string translation trait we can use test doubles for the translation part and then we simple simply proxy the calls to this function from our form to our custom class so let's do that I'm gonna create a new it's just a new PHP file I'm gonna call it a CSV validator relation see you live it so I can use anything that's something and then extend anything we will just have a public static function validate and here we will use the file interface again and we can simply copy this yeah and use a string translation trade oops and I'm gonna use itself see the idea would be that here we should return to CSV validator validate file now if you know the Drupal API you will know that this cannot work the reason is is you seek phpstorm is highlighting it non static method t shouldn't be called statically oops indeed the string translation trait gives us access to the T method but it's not static we should call it like this but if you want to call it like this we cannot have a static method called validate see here it works so here we should actually instantiate our and an instance of our class before we call validate so either we can call like validates or equals new CC validator but a method I prefer in this particular case is to define a service so if we go here you can actually create a services file notation service is its service for services noise cookie check maybe services need make sense actually when you think of it so serve this service is perform validation of a module I'm going to call it a CSV well teacher and the class will be our new class and then the way we call this is that we return to full service when I reuse this together later so now this could work but again it's not gonna work for a very simple reason topic or container is not initialized yet so we're still running into another issue obviously I'm going through all this to show you what it takes to set up a unit test but once you know all this it's actually much easier and a container builder so what we need to do is in our setup method before we run a test we need to create a new container so in our container we're gonna create which register let me create validate or you see the validator and then I want to register my service and a Drupal set troops set container container now we run into another issue which is we don't have the strength translation service now this luckily for us the string translation service is actually really really simple we don't really need it in our particular case we just can use a dummy empty empty testable so let me just quickly check which it's the translation interface that's the one so we are going to use a mock case which is a translation interface plots and then container you set and here we have an issue again so don't be discouraged like she's showing you all this for a good reason it takes a little bit of practice to use unit tests in Drupal it takes a lot more mocking a lot more to set up but it's really really worth it because you're in a tests are much much faster than criminal tests and for something like this what I could form where you don't actually need to write any data like entities using unit tests makes a lot of sense of course it's a bit more complex than using kernel tests because you pull is complex but trust me it's really worth it so the error we get and I don't you noticed we expect a string but what do we get we get a translatable markup object what does that mean well here we're actually using this T this T doesn't return a string like it didn't Drupal seven it returns at a class so this assertion is not going to work so there are two ways we can go about this we can either make sure that this renders is a string but this will mean we need to mock a lot of other stuff in Drupal so it's going to be complicated or we go to the quick and dirty way we actually use T around this guy which is what I prefer so it's not the best way to do this in a unit test but in our case it will suffice so the way I'm gonna use I'm gonna do this is I'm gonna actually make my unit test translation aware now that I using this trait I can actually do this and finally repeat so now we can actually start writing our logic so if I remove this now our test should be read exactly the way we want do this we're gonna use I'm gonna open the file get a Qri I'm gonna open it in your read mode and the first test is about the format so analyze the file format we get two columns I'm gonna use the F get CSV function CSV pass it a file handler and if empty the row or accounts it's smaller than 2 this is reasonable a reasonable expectation say okay if the row is empty or if it's not an array it's less than 2 then we have now will actually not go any further than this so we can I just return an array directly we will need this one later it up put it there so here I'm gonna return off I should have kept that let me copy it like this so we return our error and here boom we're green again gate great so the next thing we want to check is we want to validate that to CSV actually contains two columns for each row and we want to give an error message for every road it has an issue so if the first row is an issue and the third row is an issue we don't want to return an error message just for the first row we want to go all the way through so let's create a new file which is incorrect data sees you so I'm just gonna copy paste so this time we're going to use Camas as you would expect but I want to do two things so in the first row there is no author so this is incorrect let me even remove it like this so there is no second column at all and in this case we have the first column which exists but is a penalty so this should give us two errors so what we do is that we can copy all this this time we're going for the incorrect data CSV and we're going to get two error messages so how should we do it so the author on line one is empty you must at least one practice and then we want the second error so why not file the second one it's not this guy it's this one the second one we don't have the title so the book title on line two is empty you must provide a title for each book so if we try this again okay so our expectation is incorrect as we would expect let's write our logic so here we can actually go into the errors part so while let's go through each row row equals it gets easy let's use a quick and dirty trick here so this might give us an error for example if the lists if the road is completely empty so I can suppress that this is really not something you should do as I say you must know the rules before you can break them so I know the rules that's why I say ok I'm gonna break this one I'm gonna suppress the air if this is not an array or if this is an array with only one item in it but again be very very careful when you suppress warnings like this so if MC title we want the error as you can say so actually let me go like this so the title is empty we want to use this error message and you will notice sure damn careful that we're gonna run into an issue which is what I expect author let's get the other message the author on line one is empty but oh of course we're not going to hard-code this that doesn't make sense so that's quick to get this we should at least okay this is correct again but obviously we don't want to hard-code this value we want to use placeholders and when we use a placeholder we need something to compute this line so line is what what value oh let's just get an iterator Julie I and will initiate it here I equals zero and every time we increment I can increase start with zero because the first row is the the headers actually I'm that's wrong I cannot start with zero yes I can start wizard sorry I can serve the zero if I increment first that's where I was going because we already have the headers so the first row is already being computed here so I already fetched it so as long as I increment before I could start with zero but now this doesn't work anymore the reason is that we are now comparing objects and of course our object now is no longer the same so what we can do again quick-and-dirty is that the first message is the title so I can replace this with this version and I know that the line is one and in this case the author I know the line sorry the title of the line is 2 and the author the line is 1 it's like this now it works great so as you see you were writing tests then we're writing logic writing tests and we're writing logic so we're using true TDD practices now the final thing we want to check is as well if the file is indeed correct we should not get any errors at all so this is our final check and here we're going to use Christ's record a new fixture let me copy this one and this time is going to be books correct see so we want to use exactly that's really before but this time we want to use actual commas so here we should not get any errors at all and indeed it is correct so if we pass a correct CSV file we're not getting any issues so now what do you think of this we actually wrote a test a unit test that is checking the validation for this field as you notice we're not testing via the interface we're not using a functional test are we not using will be a test for the matter we're actually only using a unit test but we know that as long as this one as this test passes the validation for the file upload will pass as well so do we actually want do we want to have a test that checks if our form is working correctly well it's a nice to have but I would argue that no you do not the reason you do not is that the the build form method what do you do what are you doing there's no true logic in here it's actually configuration because we're using the Drupal API so there's not really any added value to check that the form is actually being rendered correctly but the fields are there that you can use the fields and if you can click on the submit button there's no added value to testing that because that's the Drupal idea you don't want to test the Drupal API you want to test your own modules logic so that's why I say when you build a form don't bother writing a functional test go for the unit test over the quickest and easiest way to write a test because the faster your test will be to run the more time you will have to write more tests to add more test coverage and to use testament driven development practices so just maybe let's compute some test coverage result so this is going to take a little bit more time I just want to while this is computing just check this 400 milliseconds an 8 megabyte of memory that's nothing I mean if if we had done the same thing which is just now with a functional test it would have taken I guess at very least 30 seconds maybe even more probably a lot more remnant well memory maybe you're not really concerned about what the time you are when you want to use TDD it has to be super super fast and unit tests provide you with that speed so oh I know I forgot something so this is a small trick that I use before I run the tests I actually changed the PHP unit XML file so let me create a copy HP units XML and we want to change this that's why it takes so long the test coverage if it needs to compute it for the whole Drupal core it's gonna take a lot of time but we're not interested in that we only want to know what's in our own modules so usually from most projects you would put your own modules in custom and contribute trip so I then even go as far as only compute for our custom module in this case it's directly it's at a root of the modules folder so I'll just keep it like this let's run this again let me see it this time it will be much faster boom okay area I always forget where it cuts it I can't sync or no Oh same car it's a turret they open this so wow it computed for tests obviously we don't care about that but let's go to sirs source we see that our CSV validator class as a 100% test coverage that's not bad the form is at zero right now but we'll get back to that later and it's only computing classes but as you noticed we're not actually calling the class itself we're calling this function so if it computed for this file as well we'd actually get a hundred percent for this file as well which is really nice so still in total were almost at 70 percent test coverage and I will show you later that's oh sorry we're at 40 percent while at 40 percent but in the later tutorials you will see that we'll get much much higher than that because we're also going to test this guy so that's it for now I hope you enjoyed this tutorial it's a bit long the next parts will be probably much faster because really we won't need all the the set-up time and a lot of stuff is already taken care of but yeah I really hope this will help you to write more unit tests and also that shows you that TDD will to plate is possible it is possible without using functional tests but using unit tests choose
Info
Channel: Wouter Admiraal
Views: 2,834
Rating: 4.7446809 out of 5
Keywords: Drupal, TDD, Tutorial
Id: iK15gqkV9oc
Channel Id: undefined
Length: 42min 44sec (2564 seconds)
Published: Mon Jan 29 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.