3

I am having trouble to unit test Symfony forms that have an 'entity' field inside.

I found potential solutions here and here, however I could not make them 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 the previous solutions I have 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\'s region',
                'attr' => array('class' => 'new_city_combo_box')
            )
        );
}

Currently, I get this error:

1) FormsTest::testSubmitValidData Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException: The options "attr", "class", "empty_value", "label", "property", "query_builder", "required" do not exist. Known options are: "".

I need to have these forms covered with tests, and I am completely out of ideas. Any help ?

2
  • The error comes from the form declaration, not your test. See the entity field documentation, the options attr, empty_value, etc. are not available for this type of field. But class is available so this may not be the reason. Commented Jun 23, 2015 at 15:09
  • Please take a look at this issue: github.com/symfony/symfony/issues/15098. I think a EntityType should be testable out of the box. Commented Oct 9, 2015 at 7:50

3 Answers 3

2

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,
                )
            );
        }
    )
);
Sign up to request clarification or add additional context in comments.

1 Comment

With this setting i get an TypeError: Argument 1 passed to Symfony\Bridge\Doctrine\Form\Type\DoctrineType::__construct() must implement interface Doctrine\Common\Persistence\ManagerRegistry, none given, called in /var/www/shn/vendor/symfony/symfony/src/Symfony/Component/Form/FormRegistry.php on line 85 with Symfony 3.0.6 Of course i changed setDefaultOptions to configureOptions
0

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

documentation

Comments

0

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' => '[email protected]',
            '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());
    }

}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.