Write Test for Custom Validation Constraint in Symfony 8

Write Test for Custom Validation Constraint in Symfony 8

Validation rules are often at the heart of business logic. Whether you're checking user input, enforcing domain constraints, or validating API requests, custom validators help keep the application consistent and reliable. However, writing the constraint itself is only half the job - verifying that it behaves correctly under all conditions is just as important. This tutorial explains how to write test for custom validation constraint in Symfony 8 application.

Imagine a situation where you want to validate that a value is always an even number. To achieve this, we can define a custom constraint and a corresponding validator.

The constraint class is responsible for holding configuration such as the default error message. It is declared as a PHP attribute so it can be easily attached to properties.

src/Validator/Constraints/EvenNumber.php

<?php

namespace App\Validator\Constraints;

use Attribute;
use Symfony\Component\Validator\Constraint;

#[Attribute(Attribute::TARGET_PROPERTY)]
class EvenNumber extends Constraint
{
    public string $message = 'The value "{{ value }}" must be an even number.';
}

The validator contains the actual logic that determines whether a value is valid. It checks for incorrect constraint usage and validates whether the number is even.

src/Validator/Constraints/EvenNumberValidator.php

<?php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

class EvenNumberValidator extends ConstraintValidator
{
    public function validate(mixed $value, Constraint $constraint): void
    {
        if (!($constraint instanceof EvenNumber)) {
            throw new UnexpectedTypeException($constraint, EvenNumber::class);
        }
        if (null === $value || '' === $value) {
            return; // 'NotBlank' or 'NotNull' constraint should be care of that
        }
        if (!is_numeric($value)) {
            return; // 'Type' constraint should be care of that
        }

        if ((int) $value % 2 !== 0) {
            $this->context
                ->buildViolation($constraint->message)
                ->setParameter('{{ value }}', (string) $value)
                ->addViolation();
        }
    }
}

Symfony provides the ConstraintValidatorTestCase class, which removes much of the boilerplate usually required when testing validators. It gives you access to assertion helpers and a preconfigured validation context.

The test class focuses on verifying both valid and invalid inputs, as well as ensuring the validator reacts correctly when used with the wrong constraint type.

tests/Validator/Constraints/EvenNumberValidatorTest.php

<?php

namespace App\Tests\Validator\Constraints;

use App\Validator\Constraints\EvenNumber;
use App\Validator\Constraints\EvenNumberValidator;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;

class EvenNumberValidatorTest extends ConstraintValidatorTestCase
{
    protected function createValidator(): ConstraintValidatorInterface
    {
        return new EvenNumberValidator();
    }

    #[DataProvider('invalidDataProvider')]
    public function testInvalid(mixed $value): void
    {
        $constraint = new EvenNumber();
        $this->validator->validate($value, $constraint);

        $this
            ->buildViolation($constraint->message)
            ->setParameter('{{ value }}', (string) $value)
            ->assertRaised();
    }

    public static function invalidDataProvider(): array
    {
        return [
            'Int type' => [11],
            'String type' => ['11'],
        ];
    }

    #[DataProvider('validDataProvider')]
    public function testValid(mixed $value): void
    {
        $this->validator->validate($value, new EvenNumber());
        $this->assertNoViolation();
    }

    public static function validDataProvider(): array
    {
        return [
            'Null' => [null],
            'Empty string' => [''],
            'Not numeric' => ['ABC'],
            'Int type' => [10],
            'String type' => ['10'],
        ];
    }

    public function testInvalidWhenWrongConstraintType(): void
    {
        $this->expectException(UnexpectedTypeException::class);
        $this->validator->validate(null, new NotBlank());
    }
}

Leave a Comment

Cancel reply

Your email address will not be published.