(Using framework Symfony 4.4)
I try to learn about how to test a Form having a field being an EntityType form field.
See exemple bellow :
class VaccinationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('vaccine_brand_uid', EntityType::class, ['class' => ViewVaccineBrand::class])
->add('administration_date', DateTimeType::class, [
'widget' => 'single_text',
'model_timezone' => 'UTC',
'view_timezone' => 'UTC',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => VaccinationInput::class,
'method' => 'POST',
'csrf_protection' => false
]);
}
}
As you can see the field vaccine_brand_uid is an EntityType field so it ensure the given value when submitting the form is part of the ViewVaccineBrand table.
Here's bellow the related VaccinationInput object :
namespace App\Services\Domain\Vaccination\DTO;
use App\Entity\ViewVaccineBrand;
...
class VaccinationInput
{
/**
* @Assert\Type(type=ViewVaccineBrand::class, message="api_missingBrand")
* @Assert\NotBlank(message="api_missingBrand")
* @var ViewVaccineBrand|int
*/
public $vaccine_brand_uid;
/**
* @Assert\DateTime(message="api_missingAdministrationDate")
*/
public $administration_date;
}
Create a base class for testing form with EntityType fields
So while trying to create a test class for this form, I found this exemple in the Symfony repository : https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php
So I copy/paste this class to adapt it for my own tests !
And I adapt it a little so it is more reusable (adding the getTypes() function) :
/**
* Inspired by https://github.com/symfony/symfony/blob/4.4/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php
*/
abstract class EntityTypeTestCase extends TypeTestCase
{
/**
* @var EntityManager
*/
protected $em;
/**
* @var MockObject&ManagerRegistry
*/
protected $emRegistry;
protected function setUp(): void
{
$this->em = DoctrineTestHelper::createTestEntityManager();
$this->emRegistry = $this->createRegistryMock('default', $this->em);
parent::setUp();
$schemaTool = new SchemaTool($this->em);
$classes = array_map(fn($c) => $this->em->getClassMetadata($c), $this->registeredTypes());
try {
$schemaTool->dropSchema($classes);
} catch (\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch (\Exception $e) {
}
}
protected function tearDown(): void
{
parent::tearDown();
$this->em = null;
$this->emRegistry = null;
}
protected function getExtensions(): array
{
return array_merge(parent::getExtensions(), [
new DoctrineOrmExtension($this->emRegistry),
]);
}
protected function createRegistryMock($name, $em): ManagerRegistry
{
$registry = $this->createMock(ManagerRegistry::class);
$registry->expects($this->any())
->method('getManager')
->with($this->equalTo($name))
->willReturn($em);
return $registry;
}
protected function persist(array $entities)
{
foreach ($entities as $entity) {
$this->em->persist($entity);
}
$this->em->flush();
}
protected function getTypes()
{
return array_merge(parent::getTypes(), []);
}
/**
* @return array An array of current registered entity type classes.
*/
abstract protected function registeredTypes(): array;
}
Create a specific test class for my Vaccination form
So I want to test my VaccinationType form, here's what I did bellow.
~/symfony/tests/Service/Domain/Vaccination/Type/VaccinationTypeTest
<?php
namespace App\Tests\Service\Domain\Vaccination\Type;
...
class VaccinationTypeTest extends EntityTypeTestCase
{
public function testWhenMissingBrandThenViolation()
{
$model = new VaccinationInput();
$entity1 = (new ViewVaccineBrand())->setVaccineBrandName('test')->setIsActive(true)->setVaccineCode('code');
$this->persist([$entity1]);
// $model will retrieve data from the form submission; pass it as the second argument
$form = $this->factory->create(VaccinationType::class, $model);
$form->submit(['vaccine_brand_uid' => 2, 'administration_date' => DateTime::formatNow()]);
$violations = $form->getErrors(true);
$this->assertCount(1, $violations); // There is no vaccine brand for uid 2
}
protected function getTypes()
{
return array_merge(parent::getTypes(), [new EntityType($this->emRegistry)]);
}
protected function registeredTypes(): array
{
return [
ViewVaccineBrand::class,
ViewVaccineCourse::class,
];
}
protected function getExtensions(): array
{
$validator = Validation::createValidator();
return array_merge(parent::getExtensions(), [
new ValidatorExtension($validator),
]);
}
}
Actual result
The actual result of the php bin/phpunit execution is as follow :
There was 1 error:
- App\Tests\Service\Domain\Vaccination\Type\VaccinationTypeTest::testWhenMissingBrandThenViolation Symfony\Component\Form\Exception\RuntimeException: Class "App\Entity\ViewVaccineBrand" seems not to be a managed Doctrine entity. Did you forget to map it?
/app/xxxxapi/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:203 /app/xxxxapi/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:203 /app/xxxxapi/vendor/symfony/options-resolver/OptionsResolver.php:1035 /app/xxxxapi/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:130 /app/xxxxapi/vendor/symfony/options-resolver/OptionsResolver.php:915 /app/xxxxapi/vendor/symfony/options-resolver/OptionsResolver.php:824 /app/xxxxapi/vendor/symfony/form/ResolvedFormType.php:97 /app/xxxxapi/vendor/symfony/form/FormFactory.php:76 /app/xxxxapi/vendor/symfony/form/FormBuilder.php:94 /app/xxxxapi/vendor/symfony/form/FormBuilder.php:244 /app/xxxxapi/vendor/symfony/form/FormBuilder.php:195 /app/xxxxapi/vendor/symfony/form/FormFactory.php:30 /app/xxxxapi/tests/Service/Domain/Vaccination/Type/VaccinationTypeTest.php:23
I think that's because for some reason, the entitymanager created in the EntityTypeTestCase is not the same as the one used at /app/xxxxx/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:203 ...
But I don't know how to specify that, in the case of this test, I want that DoctrineType (which is parent of EntityType) use this special test entityManager instead of the default one.
Expected result
Make the test work an the assertion should be successfull.
Edit
I add the Entity for extra informations
namespace App\Entity;
/**
* VaccineBrand
*
* @ORM\Table(name="dbo.V_HAP_VACCINE_BRAND")
* @ORM\Entity(repositoryClass="App\Repository\ViewVaccineBrandRepository")
*/
class ViewVaccineBrand
{
/**
* @var int
*
* @ORM\Column(name="VACCINE_BRAND_UID", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $vaccineBrandUid;
/**
* @var string
* @ORM\Column(name="VACCINE_BRAND_NAME", type="string", nullable=false)
*/
protected $vaccineBrandName;
/**
* @var string
* @ORM\Column(name="VACCINE_BRAND_CODE", type="string", nullable=true)
*/
protected $vaccineBrandCode;
// ... Other fields are not relevant
}
PS
I already read those one with no luck :
- ==> https://stackoverflow.com/a/49879186/6503197 This one is exactly what I tried and does not work. Also you can see reading the answer that there is no validated solutions.
- ==> https://symfony.com/doc/current/form/unit_testing.html#testing-types-registered-as-services The official doc is not really precise on how to do things in this case, although it mentions DoctrineOrmExtension without explaining more.
This is a complicated situation, so I will provide an abbreviated outline of what I believe you need to do.
EntityType(for related entity) toChoiceTypeCallbackChoiceLoaderthat queries the DB for all the uid fields indexed by name (or whatever you want to be the label), i.e.:['BigPharm'=>123, 'Other Brand'=>564, ]@Assert\Typeto@Assert\Choicecallbackoption with a method that will callarray_valueson the result of the same as theCallbackChoiceLoaderabove, i.e.:[123, 564, ]