From 14f3252d0689e328af4a05560065833e9e8d32fd Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 27 Feb 2023 17:28:54 +0100 Subject: [PATCH] [Validator] Add `CompoundConstraintTestCase` to ease testing Compound Constraints --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Test/CompoundConstraintTestCase.php | 123 ++++++++++++++++++ .../Constraints/CompoundValidatorTest.php | 15 +-- .../Fixtures/DummyCompoundConstraint.php | 30 +++++ .../DummyCompoundConstraintWithGroups.php | 28 ++++ .../Test/CompoundConstraintTestCaseTest.php | 85 ++++++++++++ 6 files changed, 268 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Component/Validator/Test/CompoundConstraintTestCase.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraint.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraintWithGroups.php create mode 100644 src/Symfony/Component/Validator/Tests/Test/CompoundConstraintTestCaseTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index d8ae5e6cc1671..b9af46e493aa9 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Add the `format` option to the `Ulid` constraint to allow accepting different ULID formats * Add the `WordCount` constraint * Add the `Week` constraint + * Add `CompoundConstraintTestCase` to ease testing Compound Constraints 7.1 --- diff --git a/src/Symfony/Component/Validator/Test/CompoundConstraintTestCase.php b/src/Symfony/Component/Validator/Test/CompoundConstraintTestCase.php new file mode 100644 index 0000000000000..7fb32459586e0 --- /dev/null +++ b/src/Symfony/Component/Validator/Test/CompoundConstraintTestCase.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Test; + +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\CompoundValidator; +use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\Validator\Context\ExecutionContext; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * A test case to ease testing Compound Constraints. + * + * @author Alexandre Daubois + */ +abstract class CompoundConstraintTestCase extends TestCase +{ + protected ValidatorInterface $validator; + protected ?ConstraintViolationListInterface $violationList = null; + protected ExecutionContextInterface $context; + protected string $root; + + private mixed $validatedValue; + + protected function setUp(): void + { + $this->root = 'root'; + $this->validator = $this->createValidator(); + $this->context = $this->createContext($this->validator); + } + + protected function validateValue(mixed $value): void + { + $this->validator->inContext($this->context)->validate($this->validatedValue = $value, $this->createCompound()); + } + + protected function createValidator(): ValidatorInterface + { + return Validation::createValidator(); + } + + protected function createContext(?ValidatorInterface $validator = null): ExecutionContextInterface + { + $translator = $this->createMock(TranslatorInterface::class); + $translator->expects($this->any())->method('trans')->willReturnArgument(0); + + return new ExecutionContext($validator ?? $this->createValidator(), $this->root, $translator); + } + + public function assertViolationsRaisedByCompound(Constraint|array $constraints): void + { + if ($constraints instanceof Constraint) { + $constraints = [$constraints]; + } + + $validator = new CompoundValidator(); + $context = $this->createContext(); + $validator->initialize($context); + + $validator->validate($this->validatedValue, new class($constraints) extends Compound { + public function __construct(private array $testedConstraints) + { + parent::__construct(); + } + + protected function getConstraints(array $options): array + { + return $this->testedConstraints; + } + }); + + $expectedViolations = iterator_to_array($context->getViolations()); + + if (!$expectedViolations) { + throw new ExpectationFailedException(\sprintf('Expected at least one violation for constraint(s) "%s", got none raised.', implode(', ', array_map(fn ($constraint) => $constraint::class, $constraints)))); + } + + $failedToAssertViolations = []; + reset($expectedViolations); + foreach ($this->context->getViolations() as $violation) { + if ($violation != current($expectedViolations)) { + $failedToAssertViolations[] = $violation; + } + + next($expectedViolations); + } + + $this->assertEmpty( + $failedToAssertViolations, + \sprintf('Expected violation(s) for constraint(s) %s to be raised by compound.', + implode(', ', array_map(fn ($violation) => ($violation->getConstraint())::class, $failedToAssertViolations)) + ) + ); + } + + public function assertViolationsCount(int $count): void + { + $this->assertCount($count, $this->context->getViolations()); + } + + protected function assertNoViolation(): void + { + $violationsCount = \count($this->context->getViolations()); + $this->assertSame(0, $violationsCount, \sprintf('No violation expected. Got %d.', $violationsCount)); + } + + abstract protected function createCompound(): Compound; +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CompoundValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CompoundValidatorTest.php index 2f48657b21fc5..9eb5c7add7571 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CompoundValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CompoundValidatorTest.php @@ -11,11 +11,9 @@ namespace Symfony\Component\Validator\Tests\Constraints; -use Symfony\Component\Validator\Constraints\Compound; use Symfony\Component\Validator\Constraints\CompoundValidator; -use Symfony\Component\Validator\Constraints\Length; -use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Tests\Fixtures\DummyCompoundConstraint; class CompoundValidatorTest extends ConstraintValidatorTestCase { @@ -43,14 +41,3 @@ public function testValidateWithConstraints() $this->assertNoViolation(); } } - -class DummyCompoundConstraint extends Compound -{ - protected function getConstraints(array $options): array - { - return [ - new NotBlank(), - new Length(['max' => 3]), - ]; - } -} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraint.php b/src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraint.php new file mode 100644 index 0000000000000..87253f25d93a6 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraint.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; + +class DummyCompoundConstraint extends Compound +{ + protected function getConstraints(array $options): array + { + return [ + new NotBlank(), + new Length(['max' => 3]), + new Regex('/[a-z]+/'), + new Regex('/[0-9]+/'), + ]; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraintWithGroups.php b/src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraintWithGroups.php new file mode 100644 index 0000000000000..8b2693ff6ad15 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/DummyCompoundConstraintWithGroups.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures; + +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; + +class DummyCompoundConstraintWithGroups extends Compound +{ + protected function getConstraints(array $options): array + { + return [ + new NotBlank(groups: ['not_blank']), + new Length(['max' => 3], groups: ['max_length']), + ]; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Test/CompoundConstraintTestCaseTest.php b/src/Symfony/Component/Validator/Tests/Test/CompoundConstraintTestCaseTest.php new file mode 100644 index 0000000000000..da62992be9875 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Test/CompoundConstraintTestCaseTest.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Test; + +use PHPUnit\Framework\ExpectationFailedException; +use Symfony\Component\Validator\Constraints\Compound; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Component\Validator\Test\CompoundConstraintTestCase; +use Symfony\Component\Validator\Tests\Fixtures\DummyCompoundConstraint; + +class CompoundConstraintTestCaseTest extends CompoundConstraintTestCase +{ + protected function createCompound(): Compound + { + return new DummyCompoundConstraint(); + } + + public function testAssertNoViolation() + { + $this->validateValue('ab1'); + + $this->assertNoViolation(); + $this->assertViolationsCount(0); + } + + public function testAssertIsRaisedByCompound() + { + $this->validateValue(''); + + $this->assertViolationsRaisedByCompound(new NotBlank()); + $this->assertViolationsCount(1); + } + + public function testMultipleAssertAreRaisedByCompound() + { + $this->validateValue('1245'); + + $this->assertViolationsRaisedByCompound([ + new Length(max: 3), + new Regex('/[a-z]+/'), + ]); + $this->assertViolationsCount(2); + } + + public function testNoAssertRaisedButExpected() + { + $this->validateValue('azert'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage("Expected violation(s) for constraint(s) Symfony\Component\Validator\Constraints\Length, Symfony\Component\Validator\Constraints\Regex to be raised by compound."); + $this->assertViolationsRaisedByCompound([ + new Length(max: 5), + new Regex('/^[A-Z]+$/'), + ]); + } + + public function testAssertRaisedByCompoundIsNotExactlyTheSame() + { + $this->validateValue('123'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Expected violation(s) for constraint(s) Symfony\Component\Validator\Constraints\Regex to be raised by compound.'); + $this->assertViolationsRaisedByCompound(new Regex('/^[a-z]+$/')); + } + + public function testAssertRaisedByCompoundButGotNone() + { + $this->validateValue('123'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Expected at least one violation for constraint(s) "Symfony\Component\Validator\Constraints\Length", got none raised.'); + $this->assertViolationsRaisedByCompound(new Length(max: 5)); + } +}