Unit testing Symfony forms with entities Unit testing Symfony forms with entities symfony symfony

Unit testing Symfony forms with entities


This is happening because the OptionsResolver is not aware of those options. 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,                )            );        }    ));


'property' option has been replaced by 'choice_label' from symfony 2.7 onwards.

documentation


I would like to introduce my solution. At first, I am injecting the entity type into my form. Here is what I did in my UserBundle, which extends the 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, which only provides the required methods to work like an 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());    }}