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 |
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.
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!
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.
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!
Hi sadikoff!
I would rather not do it like that because I need to change the mock between tests. Any thoughts? Thanks!
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!
I think you should add $client->request('GET', '/'); again after you submit the form. Otherwise you'll get a redirect content page.
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!
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
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!
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
You don't have to delete last char in class name, just press alt + ender and in popup, chose import class
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 :)
// 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
}
}
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!