Unit Testing Symfony Forms with Objects
I am having problems with unit test Symfony forms that have an "entity" field inside.
I found potential solutions here and here , however I couldn't get them to work.
Here's my code:
FormsTest.php
protected function setUp()
{
parent::setUp();
$this->factory = Forms::createFormFactoryBuilder()
->addExtensions($this->getExtensions())
->getFormFactory();
}
protected function getExtensions()
{
$mockEntityType = $this->getMockBuilder('Symfony\Bridge\Doctrine\Form\Type\EntityType')
->disableOriginalConstructor()
->getMock();
$mockEntityType->expects($this->any())->method('getName')
->will($this->returnValue('entity'));
return array(new PreloadedExtension(array(
$mockEntityType->getName() => $mockEntityType,
), array()));
}
public function testSubmitValidData()
{
$formData = array(
'name' => 'Mbalmayo',
'latitude' => 3.5165475,
'longitude' => 11.5144015,
'zoomLevel' => 12.0,
'region' => 'Centre',
);
$type = new CitiesType();
$form = $this->factory->create($type, null);
$object = new Cities();
$object->fromArray($formData);
// submit the data to the form directly
$form->submit($formData);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($object, $form->getData());
$view = $form->createView();
$children = $view->children;
foreach (array_keys($formData) as $key) {
$this->assertArrayHasKey($key, $children);
}
}
This code is based on previous solutions found.
CitiesType.php
/**
* Builds the form data for the cities
*
* @param FormBuilderInterface $builder The FormBuilderInterface to use
* @param array $options The options for the form, if any
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
/* Several adds that are pointless for this problem */
->add('region', 'entity', array('class' => 'SmopaAgentFinderBundle:Regions',
'property' => 'name',
'required' => true,
'label' => 'city.new.region',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('r')
->orderBy('r.name', 'ASC');
},
'empty_value' => 'Select city\ region',
'attr' => array('class' => 'new_city_combo_box')
)
);
}
I am currently getting this error:
1) FormsTest :: testSubmitValidData Symfony \ Component \ OptionsResolver \ Exception \ UndefinedOptionsException: The parameters "attr", "class", "empty_value", "label", "property", "query_builder", "required" do not exist. The following options are known: "".
I need these forms to be embraced by trials and I'm completely out of ideas. Any help?
source to share
This is because the OptionsResolver does not know these parameters. Your EntityType mock should declare them:
For Symfony 2.7:
// use Symfony\Component\OptionsResolver\OptionsResolver;
$mockEntityType->method('setDefaultOptions')->will(
$this->returnCallback(
function (OptionsResolver $resolver)
{
$resolver->setDefaults(
array(
'choice_label' => null,
'class' => null,
'query_builder' => null,
'required' => null,
)
);
}
)
);
source to share
I would like to present my solution. First, I injected the entity type into my form. Here's what I did in my UserBundle, which extends FOSUserBundle, because I needed to assign groups to users:
namespace UserBundle\Form\Type;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use UserBundle\Entity\Group;
/**
* Class UserType
* @package UserBundle\Form\Type
*/
class UserType extends AbstractType
{
/**
* @var array
*/
private $roles;
/**
* GroupType constructor.
* @param $roles
*/
public function __construct($roles)
{
$this->roles = $roles;
}
/**
* @var EntityType
*/
private $entityType;
/**
* @param EntityType $entityType
* @internal param $imagepath
*/
public function setEntityType(
$entityType
)
{
$this->entityType = $entityType;
}
/**
* Mainly for testing
* @return bool
*/
public function hasEntityType(){
return $this->entityType instanceof EntityType;
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('enabled',
CheckboxType::class,
[
'label' => 'smartadmin.user.active',
'required' => false
]
)->add('groups',
$this->entityType,
[
'class' => Group::class,
'property' => 'name',
'multiple' => true,
'expanded' => true,
'required' => false,
'label' => 'smartadmin.user.groups'
]
)->add('username',
TextType::class,
[
'label' => 'form.username',
'translation_domain' => 'FOSUserBundle'
]
)->add('email',
EmailType::class,
[
'label' => 'form.email',
'translation_domain' => 'FOSUserBundle',
'constraints' => [new Email()]
]
)->add('firstname',
TextType::class,
[
'label' => 'smartadmin.user.firstname'
]
)->add('lastname',
TextType::class,
[
'label' => 'smartadmin.user.lastname'
]
)->add('gender',
ChoiceType::class,
[
'label' => 'smartadmin.user.gender',
'choices' => [1 => 'smartadmin.user.gender_male', 2 => 'smartadmin.user.gender_female']
]
)->add('plainPassword', RepeatedType::class,
[
'type' => PasswordType::class,
'options' => array('translation_domain' => 'FOSUserBundle'),
'first_options' => array('label' => 'form.new_password'),
'second_options' => array('label' => 'form.new_password_confirmation'),
'invalid_message' => 'fos_user.password.mismatch',
'required' => false
]
);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'UserBundle\Entity\User',
'csrf_token_id' => 'profile'
));
}
}
And in my controller:
$oUser = $this->userRepository->find($userId);
$userType = new UserType($this->roleService->getAvailableRoles());
$userType->setEntityType(new EntityType($this->doctrine));
$oForm = $this->formFactory->createBuilder($userType)
->setData($oUser)
->getForm();
To test this form, I created a replacement entity type that provides only the necessary methods to act as entityType:
namespace UserBundle\Tests\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Util\StringUtil;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* Class EntityTypeSimulator
* @package UserBundle\Tests\Form\Type
*/
class EntityTypeSimulator extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function finishView(FormView $view, FormInterface $form, array $options)
{
}
/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
if (!$resolver instanceof OptionsResolver) {
throw new \InvalidArgumentException(sprintf('Custom resolver "%s" must extend "Symfony\Component\OptionsResolver\OptionsResolver".', get_class($resolver)));
}
$this->configureOptions($resolver);
}
/**
* Configures the options for this type.
*
* @param OptionsResolver $resolver The resolver for the options.
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'multiple' => false,
'expanded' => false,
'class' => null,
'property' => true,
'invalid_message' => null
));
}
/**
* {@inheritdoc}
*/
public function getName()
{
// As of Symfony 2.8, the name defaults to the fully-qualified class name
return get_class($this);
}
/**
* Returns the prefix of the template block name for this type.
*
* The block prefixes default to the underscored short class name with
* the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
*
* @return string The prefix of the template block name
*/
public function getBlockPrefix()
{
$fqcn = get_class($this);
$name = $this->getName();
// For BC: Use the name as block prefix if one is set
return $name !== $fqcn ? $name : StringUtil::fqcnToBlockPrefix($fqcn);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
}
}
And now my test case:
namespace UserBundle\Tests\Form\Type;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
use UserBundle\Entity\User;
use UserBundle\Form\Type\UserType;
/**
* Class UserTypeTest
* @package UserBundle\Tests\Form\Type
*/
class UserTypeTest extends TypeTestCase
{
/**
* Load the ValidatorExtension so RepeatedType can resolve 'invalid_message'
* @return array
*/
protected function getExtensions()
{
return array(new ValidatorExtension(Validation::createValidator()));
}
public function testGroupType(){
$formData['user'] = [
'enabled' => 1,
'groups' => [1,2,3],
'username' => 'Test username',
'email' => 'test@email.de',
'firstname' => 'Test Firstname',
'lastname' => 'Test Lastname',
'gender' => 1,
'plainPassword' => ['first' => 'Test Password', 'second' => 'Test Password']
];
$roles = ['ROLE_USER'];
$entity = new User('Test name');
$type = new UserType($roles);
$type->setEntityType(new EntityTypeSimulator());
$oFormBuilder = $this->factory->createBuilder();
$oFormBuilder->add('user', $type);
$oFormBuilder->setData(['user' => $entity]);
$oForm = $oFormBuilder->getForm();
$oForm->submit($formData);
$oForm->handleRequest();
$this->assertTrue($oForm->isSynchronized());
$view = $oForm->createView();
$children = $view->children['user']->children;
foreach (array_keys($formData['user']) as $key) {
$this->assertArrayHasKey($key, $children);
}
$this->assertEquals($formData['user']['username'], $entity->getUsername());
$this->assertEquals($formData['user']['email'], $entity->getEmail());
}
}
source to share