Skip to content

[Validator] Add CompoundConstraintTestCase to ease testing Compound Constraints #49547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
123 changes: 123 additions & 0 deletions src/Symfony/Component/Validator/Test/CompoundConstraintTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <alex.daubois@gmail.com>
*/
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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]),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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]+/'),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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']),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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));
}
}