Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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.

... 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="">&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').

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

Leave a comment!

15
Login or Register to join the conversation
amcastror Avatar
amcastror Avatar amcastror | posted 2 years ago

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!

Reply
amcastror Avatar

Ok, so.. my bad. I was trying to set null for the service and the "set" was ignored. Using a proper mock fixed the problem.

One other problem that I have is that I cannot get to make two requests in the same test to work, the second one looses the mock. Any hints? thanks a lot.

Reply

Hey Matias C.

Is it your custom service? if so you can create a real mock service and add it to service_test.yaml so it can be wired to your controller

Cheers!

Reply
amcastror Avatar
amcastror Avatar amcastror | sadikoff | posted 2 years ago | edited

Hi sadikoff!

I would rather not do it like that because I need to change the mock between tests. Any thoughts? Thanks!

Reply

hm can you share your tests part where your mock and where it fails? It will be easier to help with some real example =)

Cheers!

Reply
William D. Avatar
William D. Avatar William D. | posted 4 years ago

I think you should add $client->request('GET', '/'); again after you submit the form. Otherwise you'll get a redirect content page.

Reply

Hey GuitarSessions,

It depends on your needs, I see we use followRedirects() in this video because we do want to get the content of the page we redirected.

Cheers!

Reply
David R. Avatar
David R. Avatar David R. | posted 4 years ago

Hey Diego!

I'm coming back to see this tutorial as i'm currently testing my app. I'm trying to unit test all my forms. The simple forms are easy to unit test, but i'm not finding any correct way to test the forms with EntityType or CollectionType.

I've tested a lot of solutions for the EntityType, but from what i understand it looks like the EntityType depends too heavily on other Symfony process to be unit tested.

And i'm able to test the form with CollectionType without errors, but the tested data (i'm passing an ArrayCollection with correct data to the field that is CollectionType in my Form) are not retrieved by the "submit()" method of form created.

I searched a lot over the internet, but i didn't find a lot of responses about this. I'm not an expert with phpunit so i'm wondering if i'm doing something wrong or if it's not a good practice to unit test the form, and if it's better to just stick with integration or functional test.

If you could give some of your advices about that it would be really helpful :)

Thanks again for the good tutorials you're providing!
David

Reply

Hey David R.

I really love testing but I don't unit test my forms, I don't think it's needed but what I do is a functional test for them. The best way for me is creating a Behat feature file and add all the scenarios I want to test. If you are not familiar with Behat, we do have a course about it: https://symfonycasts.com/sc...
but if you don't feel like jumping on Behat right now, you can perform functional tests with PHPUnit and Symfony (just follow this link: https://symfony.com/doc/cur... )

I hope it helps you :)
Cheers!

Reply
David R. Avatar
David R. Avatar David R. | MolloKhan | posted 4 years ago | edited

Hey MolloKhan

Thanks a lot for your reply!
I'll stick with the classic functional tests for the form and after that i'll follow the Behat course.

Thanks again and keep up the great work on SC :)
David

Reply
Default user avatar

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

Reply
Default user avatar

enter*

Reply

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 :)

Reply
Thao L. Avatar
Thao L. Avatar Thao L. | posted 5 years ago

BTW the method name is followRedirects not followsRedirect

Reply

Hey Thao L.

Nice catch! thanks for informing us. It's already fixed :)

Have a nice day

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.0, <7.4",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/orm": "^2.5", // v2.7.2
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "sensio/distribution-bundle": "^5.0.19", // v5.0.21
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
        "symfony/monolog-bundle": "^3.1.0", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.6.0
        "symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
        "symfony/symfony": "3.3.*", // v3.3.13
        "twig/twig": "^1.0||^2.0" // v2.4.4
    },
    "require-dev": {
        "doctrine/data-fixtures": "^1.3", // 1.3.3
        "doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
        "liip/functional-test-bundle": "^1.8", // 1.8.0
        "phpunit/phpunit": "^6.3", // 6.5.2
        "sensio/generator-bundle": "^3.0", // v3.1.6
        "symfony/phpunit-bridge": "^3.0" // v3.4.30
    }
}
userVoice