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.
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()); }}