SymfonyのTypeTestCaseでvalidatorをモックする

SymfonyのTypeTestCaseでvalidatorをモックする

March 17, 2020,
tags: symfony form validator mock phpunit


このエントリーをはてなブックマークに追加

FormTypeで独自で定義したバリデーションを使う場合に、TypeTestCaseを使ってテストを実装するこが多いかと思います。
constraintsを設定している場合は Extensions を使うことでテストすることが出来ます。

<?php

namespace App\Form\Type;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints as Assert;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options) :void
    {
        $builder
            ->add('name', TextType::class, [
                'label' => '名前',
                'required' => true
            ])
            ->add('email', TextType::class, [
                'label' => 'メールアドレス',
                'required' => true,
                'constraints' => [
                    new Assert\Email()
                ],
            ]);

        $builder->addModelTransformer(
            new CallbackTransformer(
                static function ($user) {
                    if ($user instanceof User) {
                        return [
                            'name' => $user->getName(),
                            'email' => $user->getEmail()
                        ];
                    }
                },
                static function ($data) {
                    return new User($data['name'], $data['email']);
                }
            )
        );
    }
}
<?php

namespace App\Tests\Form\Type;

use App\Form\Type\UserType;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;

class UserTypeTest extends TypeTestCase
{
    public function testSubmit(): void
    {
        $formData = [
            'name' => 'test',
            'email' => 'polidogs@gmail.com'
        ];

        $form = $this->factory->create(UserType::class);
        $form->submit($formData);

        self::assertTrue($form->isValid());
    }

    protected function getExtensions(): array
    {
        $validator = Validation::createValidator();
        return [new ValidatorExtension($validator)];
    }
}

ここでの問題点

UserTypeカスタムなValidatorを利用し、且つValidatorがコンストラクタで引数をもつ下のようなValidatorをFormで利用してた場合は、上のテストコードではエラーになります。

<?php

namespace App\Validator\Constraints;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class DuplicateEmailValidator extends ConstraintValidator
{
    /**
     * @var UserRepository
     */
    private $repository;

    /**
     * @param UserRepository $repository
     */
    public function __construct(UserRepository $repository)
    {
        $this->repository = $repository;
    }

    public function validate($value, Constraint $constraint) :void
    {
        $user = $this->repository->findOneBy(['email' => $value]);
        if ($user instanceof User) {
            $this->context
                ->buildViolation($constraint->message)
                ->addViolation();
        }
    }
}

どう解決するのか?

いくつか解決策があるとは思いますが、今回はgetExtensions()メソッドでDuplicateEmailValidatorをモックするという方法で対応しました。

<?php

namespace App\Tests\Form\Type;

use App\Form\Type\UserType;
use App\Validator\Constraints\DuplicateEmailValidator;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\ConstraintValidatorFactory;
use Symfony\Component\Validator\Validation;

class UserTypeTest extends TypeTestCase
{
    public function testSubmit(): void
    {
        $formData = [
            'name' => 'test',
            'email' => 'polidogs@gmail.com'
        ];

        $form = $this->factory->create(UserType::class);
        $form->submit($formData);

        self::assertTrue($form->isValid());
    }

    protected function getExtensions(): array
    {
        $validatorFactory = new class() extends ConstraintValidatorFactory {
            public function setValidator($className, $object): void
            {
                $this->validators[$className] = $object;
            }
        };

        $duplicateEmailValidator = $this->prophesize(DuplicateEmailValidator::class);
        $validatorFactory->setValidator(DuplicateEmailValidator::class, $duplicateEmailValidator->reveal());


        $validator = Validation::createValidatorBuilder()
            ->setConstraintValidatorFactory($validatorFactory)
            ->getValidator();

        return [new ValidatorExtension($validator)];
    }
}

ConstraintValidatorFactoryを継承してvalidatorsプロパティの中にMockObjectを設定出来るようにします。 その後prophecyでモックを作成して、ValidatorFactoryに設定します。

あとはValidation::createValidatorBuilder()でValidatorBuilderを生成して、Factoryオブジェクトを設定して、Validatorを取得するという流れです。

バリデーションを含めてテストしなくていいのか?

「バリデーションがモックされて正しくテストできない」と考える方もいるかと思います。
本当にFormのテストでバリデーションをテストする必要があるのでしょうか?
FormTypeとしての責務はなんでしょうか?

個人的にはバリデーションは別にテストすればいいと考えています。

どこまでやるかは個人の考え方、チームの方針などによって見定めていくと良いでしょう。

最後に

これば正しい方法ではなくて、今回ボクが使用したテストの方法になります。
もっといい方法があるよというかたはぜひブログ書いてください!

参考

comments powered by Disqus