Unit testing of a Symfony custom constraint

Published on 2022-11-05 • Modified on 2022-11-05

This snippet shows how to unit test a custom Symfony validation constraint. One learns every day; I made this snippet inspired by other web resources, then I read the documentation, and I realized there is now a ConstraintValidatorTestCase which I didn't know yet and never used. This snippet is still OK; at least we see how to use mocks. So, in the following snippet, we'll see how to do the same thing using the ConstraintValidatorTestCase. The constraint I use for the example is just a dummy one, and it only checks that a string is different from the "coil" value. With these unit tests, I get 100% coverage on the custom constraint.


<?php

declare(strict_types=1);

namespace App\Tests\Unit\Validator;

use App\Entity\Article;
use App\Validator\NotCoil;
use App\Validator\NotCoilValidator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilder;

final class NotCoilTest extends TestCase
{
    /**
     * You can put this method in a trait.
     */
    private function initValidator(ConstraintValidator $validator, ?string $expectedMessage = null): void
    {
        $builder = $this->getMockBuilder(ConstraintViolationBuilder::class)
            ->disableOriginalConstructor()
            ->getMock();

        $context = $this->getMockBuilder(ExecutionContext::class)
            ->disableOriginalConstructor()
            ->getMock();

        if ($expectedMessage === null) {
            // no violation expected
            $context->expects(self::never())->method('buildViolation');
        } else {
            // a violation is expected
            $builder->expects(self::once())->method('addViolation');
            $context->expects(self::once())
                ->method('buildViolation')
                ->with(self::equalTo($expectedMessage))
                ->willReturn($builder);
        }

        /* @var ExecutionContext $context */
        $validator->initialize($context);
    }

    /**
     * Nominal cases, no validation error.
     */
    public function testNotCoilValidatorSuccess(): void
    {
        $constraint = new NotCoil();
        $validator = new NotCoilValidator();
        $this->initValidator($validator);
        $validator->validate('Foobar', $constraint);
        $validator->validate('', $constraint);
    }

    /**
     * Nominal case, a violation is raised.
     */
    public function testNotCoilValidatorViolations(): void
    {
        $constraint = new NotCoil();
        $validator = new NotCoilValidator();
        $this->initValidator($validator, 'The value should not be COil');
        $validator->validate('COil', $constraint);
    }

    /**
     * Value type failure.
     */
    public function testNotCoilValidatorUnexpectedValueException(): void
    {
        $constraint = new NotCoil();
        $validator = new NotCoilValidator();
        $this->expectException(UnexpectedValueException::class);
        $validator->validate(new Article(), $constraint); // not the good value type!
    }

    /**
     * Constraint type failure.
     */
    public function testNotCoilValidatorUnexpectedTypeException(): void
    {
        $constraint = new NotCoil();
        $validator = new NotCoilValidator();
        $this->expectException(UnexpectedTypeException::class);
        $validator->validate($constraint, new Length(['max' => 5])); // not the good constraint!
    }
}

 More on Stackoverflow   Read the doc  Random snippet

  Work with me!