Buy

Testing a Form Submit

New 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.

59 lines tests/AppBundle/Controller/DefaultControllerTest.php
... 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.

60 lines tests/AppBundle/Controller/DefaultControllerTest.php
... lines 1 - 44
public function testItGrowsADinosaurFromSpecification()
{
... lines 47 - 52
$client->followsRedirect();
... 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().

62 lines tests/AppBundle/Controller/DefaultControllerTest.php
... 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').

64 lines tests/AppBundle/Controller/DefaultControllerTest.php
... 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!

70 lines tests/AppBundle/Controller/DefaultControllerTest.php
... 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().

70 lines tests/AppBundle/Controller/DefaultControllerTest.php
... 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.

23 lines tutorial/app/Resources/views/_partials/_newDinoForm.html.twig
<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="">&nbsp;</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').

56 lines src/AppBundle/Controller/DefaultController.php
... 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.

56 lines src/AppBundle/Controller/DefaultController.php
... 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!

Leave a comment!

  • 2017-12-21 Diego Aguiar

    Hey Dawid

    Yeah, you are right, "alt + enter" is a shortcut for importing dependencies and more things (depending on where you pressed alt + enter)
    Thanks for sharing it!

    Have a nice day :)

  • 2017-12-21 Dawid

    enter*

  • 2017-12-21 Dawid

    You don't have to delete last char in class name, just press alt + ender and in popup, chose import class