Search code examples
symfonytestingintegration-testingsymfony-forms

Symfony TypeTestCase, Error: Class "doctrine.orm.validator.unique" not found


Intention: I want to test if the validations I want are in place on the School entity, for which I want to write a test class extending TypeTestCase

Questions/problems:

  1. I want to clear the error Error: Class "doctrine.orm.validator.unique" not found
  2. I want to assert the error messages for each constraints of my elements. When I remove #[UniqueEntity('name')] from the model, then problem one vanishes but still the assertion self::assertCount(1, $form->getErrors()); fails. Which means $form->getErrors() does not have the validation error for the name being blank.

I am trying to write a symfony test a symfony Form type with a DB entity, with the following (stripped) definitions:

namespace App\Entity;

use App\Repository\SchoolRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: SchoolRepository::class)]
// >>>>>>> If I remove it problem 1 will be solved 
#[UniqueEntity('name')]
class School implements TenantAwareInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private $id;

    #[Assert\NotBlank]
    #[ORM\Column(type: 'string', length: 255, unique: true)]
    private $name;
    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }
}

And form being:

namespace App\Form;

use App\Entity\School;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class SchoolType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('name');
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => School::class,
            'required' => false
        ]);
    }
}

The test:

namespace App\Tests\Integration\Form;

use App\Entity\School;
use App\Form\SchoolType;
use Doctrine\Persistence\ManagerRegistry;
use Mockery as m;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;

class SchoolTypeTest extends TypeTestCase
{
    use ValidatorExtensionTrait;

    protected function getExtensions(): array
    {
        $validator = Validation::createValidatorBuilder()
            ->enableAnnotationMapping()
            ->addDefaultDoctrineAnnotationReader()
            ->getValidator();

        $mockedManagerRegistry = m::mock(ManagerRegistry::class, ['getManagers' => []]);

        return [
            new ValidatorExtension($validator),
            new DoctrineOrmExtension($mockedManagerRegistry),
        ];
    }

    public function testValidationReturnsError()
    {
        $school = new School();
        $form = $this->factory->create(SchoolType::class, $school);

        $form->submit([]);

        self::assertTrue($form->isSynchronized());
        self::assertFalse($form->isValid());

        // >>>>>>> I want this to assert, problem 2
        self::assertCount(1, $form->getErrors());
    }
}

Solution

  • In short, I ended up writing adding a mocked UniqueEntity validator. I added some generic codes to ease testing other form types, which are as following:

    A base for tests:

    namespace App\Tests\Service;
    
    use Doctrine\Persistence\ManagerRegistry;
    use Mockery as m;
    use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator;
    use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
    use Symfony\Component\Form\FormView;
    use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
    use Symfony\Component\Form\Test\TypeTestCase;
    use Symfony\Component\Validator\Validation;
    
    class AppTypeWithValidationTestCase extends TypeTestCase
    {
        use ValidatorExtensionTrait;
    
        protected function getExtensions(): array
        {
            $mockedManagerRegistry = m::mock(
                ManagerRegistry::class,
                [
                    'getManagers' => []
                ]
            );
    
            $factory = new AppConstraintValidatorFactory();
            $factory->addValidator(
                'doctrine.orm.validator.unique',
                m::mock(UniqueEntityValidator::class, [
                    'initialize' => null,
                    'validate' => true,
                ])
            );
    
            $validator = Validation::createValidatorBuilder()
                ->setConstraintValidatorFactory($factory)
                ->enableAnnotationMapping()
                ->addDefaultDoctrineAnnotationReader()
                ->getValidator();
    
            return [
                new ValidatorExtension($validator),
                new DoctrineOrmExtension($mockedManagerRegistry),
            ];
        }
    
        // *** Following is a helper function which ease the way to 
        // *** assert validation error messages
        public static function assertFormViewHasError(FormView $formElement, string $message): void
        {
            foreach ($formElement->vars['errors'] as $error) {
                self::assertSame($message, $error->getMessage());
            }
        }
    }
    
    

    A constraint validator which accepts a validator, it is needed so we can add the (mocked) definition of UniqeEntity:

    namespace App\Tests\Service;
    
    use Symfony\Component\Validator\ConstraintValidatorFactory;
    use Symfony\Component\Validator\ConstraintValidatorInterface;
    
    class AppConstraintValidatorFactory extends ConstraintValidatorFactory
    {
        public function addValidator(string $className, ConstraintValidatorInterface $validator): void
        {
            $this->validators[$className] = $validator;
        }
    }
    

    And the final unit test class:

    <?php
    
    declare(strict_types=1);
    
    namespace App\Tests\Unit\Form;
    
    use App\Entity\School;
    use App\Form\SchoolType;
    use App\Tests\Service\AppTypeWithValidationTestCase;
    
    class SchoolTypeTest extends AppTypeWithValidationTestCase
    {
        public function testValidationReturnsError() {
            $input = [
                // *** Note that 'name' is missing here
                'is_enabled' => true,
            ];
            
            $school = new School();
            $form = $this->factory->create(SchoolType::class, $school);
    
            $form->submit($input);
    
            self::assertTrue($form->isSynchronized());
            self::assertFalse($form->isValid());
    
            $view = $form->createView();
    
            self::assertFormViewHasError($view->children['name'], 'This value should not be blank.');
        }
    }