Testing a Form Submit
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNew feature request! On the homepage, management wants a form where they can choose an enclosure, write a dinosaur spec - like "Large herbivore" and submit! Behind the scenes, we will create that new Dinosaur
and put it into the Enclosure
.
Since we're now functional-testing pros, let's get right to the test! Add public function testItGrowsADinosaurFromSpecification()
. And as usual, steal some code from earlier and paste it on top. You can start to see how some of this could be refactored to a setUp
method.
// ... lines 1 - 8 | |
class DefaultControllerTest extends WebTestCase | |
{ | |
// ... lines 11 - 44 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
$this->loadFixtures([ | |
LoadBasicParkData::class, | |
LoadSecurityData::class, | |
]); | |
$client = $this->makeClient(); | |
$crawler = $client->request('GET', '/'); | |
$this->assertStatusCode(200, $client); | |
} | |
} |
After creating the client, add $client->followRedirects()
. Normally, when our app redirects, Symfony's Client does not follow the redirect. Sometimes that's useful... but this line makes it behave like a normal browser.
// ... lines 1 - 44 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
// ... lines 47 - 52 | |
$client->followRedirects(); | |
// ... lines 54 - 57 | |
} | |
// ... lines 59 - 60 |
Filling in the Form Fields
To fill out the form fields, first we need to find the form. Do that with $form = $crawler->selectButton()
and pass this the value of the button that will be on your form. How about "Grow dinosaur". Then call ->form()
.
// ... lines 1 - 44 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
// ... lines 47 - 58 | |
$form = $crawler->selectButton('Grow dinosaur')->form(); | |
} | |
// ... lines 61 - 62 |
We now have a Form
object. No, not Symfony's normal Form object from its form system. This is from the DomCrawler
component and its job is to help us fill out its fields.
So let's think about it: we will need 2 fields: an enclosure
select field and a specification
text box. To fill in the first, use $form['enclosure']
- the enclosure
part is whatever the name
attribute for your field will be. If you're using Symfony forms, usually this will look more like dinosuar[enclosure]
.
Then, because this will be a select
field, use ->select(3)
, where 3 is the value of the option
element you want to select. Do this again for a specification
field. Setting this one is easier: ->setValue('large herbivore')
.
// ... lines 1 - 44 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
// ... lines 47 - 59 | |
$form['enclosure']->select(3); | |
$form['specification']->setValue('large herbivore'); | |
} | |
// ... lines 63 - 64 |
Honestly, I don't love Symfony's API for filling in forms - I like Mink's better. But, it works fine. When the form is ready, submit with $client->submit($form)
. That will submit to the correct URL and send all the data up!
// ... lines 1 - 44 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
// ... lines 47 - 62 | |
$client->submit($form); | |
// ... lines 64 - 67 | |
} | |
// ... lines 69 - 70 |
But... now what? What should the user see after submitting the form? Well... we should probably redirect back to the homepage with a nice message explaining what just happened. Use $this->assertContains()
to look for the text "Grew a large herbivore in enclosure #3" inside $client->getResponse()->getContent()
.
// ... lines 1 - 44 | |
public function testItGrowsADinosaurFromSpecification() | |
{ | |
// ... lines 47 - 63 | |
$this->assertContains( | |
'Grew a large herbivore in enclosure #3', | |
$client->getResponse()->getContent() | |
); | |
} | |
// ... lines 69 - 70 |
Test, done! Copy the method name and just run this test:
./vendor/bin/phpunit --filter testItGrowsADinosaurFromSpecification
Perfect! It fails with
The current node list is empty.
This is a really common error... though it's not the most helpful. It basically means that some element could not be found.
Code the Form
With the test done, let's code! And... yea, let's take a shortcut! In the tutorial/
directory, find the app/Resources/views/_partials
folder, copy it, and paste it in our app/Resources/views
directory.
<form action="{{ url('grow_dinosaur') }}" method="POST"> | |
<div class="row"> | |
<div class="column"> | |
<label for="enclosure">Enclosure</label> | |
<select name="enclosure" id="enclosure"> | |
{% for enclosure in enclosures %} | |
<option value="{{ enclosure.id }}">Enclosure #{{ enclosure.id }}</option> | |
{% endfor %} | |
</select> | |
</div> | |
<div class="column"> | |
<label for="specification">Dino description</label> | |
<input type="text" id="specification" name="specification" placeholder="Small carnivorous dino friend" /> | |
</div> | |
<div class="column"> | |
<label for=""> </label> | |
<input type="submit" class="button" value="Grow dinosaur" /> | |
</div> | |
</div> | |
</form> |
Then, at the top of index.html.twig
, use it: include('_partials/_newDinoForm.html.twig')
.
// ... lines 1 - 5 | |
use AppBundle\Factory\DinosaurFactory; | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; | |
// ... lines 8 - 11 | |
class DefaultController extends Controller | |
{ | |
// ... lines 14 - 27 | |
/** | |
* @Route("/grow", name="grow_dinosaur") | |
* @Method({"POST"}) | |
*/ | |
public function growAction(Request $request, DinosaurFactory $dinosaurFactory) | |
{ | |
$manager = $this->getDoctrine()->getManager(); | |
$enclosure = $manager->getRepository(Enclosure::class) | |
->find($request->request->get('enclosure')); | |
$specification = $request->request->get('specification'); | |
$dinosaur = $dinosaurFactory->growFromSpecification($specification); | |
$dinosaur->setEnclosure($enclosure); | |
$enclosure->addDinosaur($dinosaur); | |
$manager->flush(); | |
$this->addFlash('success', sprintf( | |
'Grew a %s in enclosure #%d', | |
mb_strtolower($specification), | |
$enclosure->getId() | |
)); | |
return $this->redirectToRoute('homepage'); | |
} | |
} |
The form is really simple: it's not even using Symfony's form system! You can see the name="enclosure"
select field where the value for each option is the enclosure's id. Below that is the name="specification"
text field and the "Grow dinosaur" button the test relies on.
For the submit logic, go back into the tutorial/
directory, find DefaultController
and copy all of the growAction()
method. Paste this into our DefaultController
. Oh, and we need a few use
statements: re-type part of @Method
and hit tab to add its use
statement. Do the same for DinosaurFactory
.
// ... lines 1 - 5 | |
use AppBundle\Factory\DinosaurFactory; | |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; | |
// ... lines 8 - 11 | |
class DefaultController extends Controller | |
{ | |
// ... lines 14 - 27 | |
/** | |
* @Route("/grow", name="grow_dinosaur") | |
* @Method({"POST"}) | |
*/ | |
public function growAction(Request $request, DinosaurFactory $dinosaurFactory) | |
{ | |
$manager = $this->getDoctrine()->getManager(); | |
$enclosure = $manager->getRepository(Enclosure::class) | |
->find($request->request->get('enclosure')); | |
$specification = $request->request->get('specification'); | |
$dinosaur = $dinosaurFactory->growFromSpecification($specification); | |
$dinosaur->setEnclosure($enclosure); | |
$enclosure->addDinosaur($dinosaur); | |
$manager->flush(); | |
$this->addFlash('success', sprintf( | |
'Grew a %s in enclosure #%d', | |
mb_strtolower($specification), | |
$enclosure->getId() | |
)); | |
return $this->redirectToRoute('homepage'); | |
} | |
} |
Ok, it's happy! Sure, the code is lacking the normal security and safeguards we expect when using Symfony's form system... but it's only a dinosaur park people! We do, however, have the success flash message!
So if we haven't messed anything up, it should work. Try the test!
./vendor/bin/phpunit --filter testItGrowsADinosaurFromSpecification
Yes! It passes! We just confirmed that this form works before we ever even loaded it in a browser. That's pretty cool.
So that's the power of functional tests. And I find them especially powerful when using Mink and testing that my JavaScript works.
Ok guys, just one more topic left, and it's fun! Continuous integration! You know, the fancy term that means: let the robots run your tests automatically!
Hi, thanks for the tutorial!
What should I do if I need to mock a service used by the controller during a functional test? Doing the same as an integration test doesn't seem to work.
Thanks!