8000 feature #49547 [Validator] Add `CompoundConstraintTestCase` to ease t… · symfony/symfony@b7fe7ef · GitHub
[go: up one dir, main page]

Skip to content

Commit b7fe7ef

Browse files
feature #49547 [Validator] Add CompoundConstraintTestCase to ease testing Compound Constraints (alexandre-daubois)
This PR was merged into the 7.2 branch. Discussion ---------- [Validator] Add `CompoundConstraintTestCase` to ease testing Compound Constraints | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #50205 | License | MIT | Doc PR | Todo This PR aims to ease compound constraints to be tested. Real world use case: we're using the Validator component to test password strength and if they match our password policy. We created a compound constraint for this. This PR allows to write easy and understandable specific tests of compound constraints, like this: ```php // src/Validator/MatchPasswordPolicy.php use Symfony\Component\Validator\Constraints as Assert; #[\Attribute] class MatchPasswordPolicy extends Assert\Compound { protected function getConstraints(array $options): array { return [ new Assert\NotBlank(allowNull: false), new Assert\Length(min: 8, max: 255), new Assert\NotCompromisedPassword(), new Assert\Type('string'), new Assert\Regex('/[A-Z]+/'), // and a few other but you get it ]; } } ``` Testing this constraint with this new TestCase helper class would result on a clear test classe: ```php // tests/Validator/MatchPasswordPolicyTest.php use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Test\CompoundConstraintTestCase; class MatchPasswordPolicyTest extends CompoundConstraintTestCase { public function createCompound(): Assert\Compound { return new MatchPasswordPolicy(); } /** * `@dataProvider` provideInvalidPasswords */ public function testInvalid(mixed $password, string $code): void { $this->validateValue($password); $this->assertViolationIsRaisedByCompound($code); } public static function provideInvalidPasswords(): \Generator { yield 'Blank' => ['', Assert\NotBlank::IS_BLANK_ERROR]; yield 'Too short' => ['a', Assert\Length::TOO_SHORT_ERROR]; yield 'Too long' => [/** Generate long string */, Assert\Length::TOO_LONG_ERROR]; yield 'Not a string' => [1, Assert\Type::INVALID_TYPE_ERROR]; yield 'No lowercase' => ['UPPER_CHAR#1', Assert\Regex::REGEX_FAILED_ERROR]; yield 'No uppercase' => ['lower_char#1', Assert\Regex::REGEX_FAILED_ERROR]; yield 'No digit' => ['no_digit_pass#', Assert\Regex::REGEX_FAILED_ERROR]; } public function testValid(): void { $this->validateValue('VeryStr0ngP4$$wOrD'); $this->assertNoViolation(); } } ``` Commits ------- 14f3252 [Validator] Add `CompoundConstraintTestCase` to ease testing Compound Constraints
2 parents 4f21f7c + 14f3252 commit b7fe7ef

File tree

6 files changed

+268
-14
lines changed

6 files changed

+268
-14
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Add the `format` option to the `Ulid` constraint to allow accepting different ULID formats
1212
* Add the `WordCount` constraint
1313
* Add the `Week` constraint
14+
* Add `CompoundConstraintTestCase` to ease testing Compound Constraints
1415

1516
7.1
1617
---
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Test;
13+
14+
use PHPUnit\Framework\ExpectationFailedException;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Validator\Constraint;
17+
use Symfony\Component\Validator\Constraints\Compound;
18+
use Symfony\Component\Validator\Constraints\CompoundValidator;
19+
use Symfony\Component\Validator\ConstraintViolationListInterface;
20+
use Symfony\Component\Validator\Context\ExecutionContext;
21+
use Symfony\Component\Validator\Context\ExecutionContextInterface;
22+
use Symfony\Component\Validator\Validation;
23+
use Symfony\Component\Validator\Validator\ValidatorInterface;
24+
use Symfony\Contracts\Translation\TranslatorInterface;
25+
26+
/**
27+
* A test case to ease testing Compound Constraints.
28+
*
29+
* @author Alexandre Daubois <alex.daubois@gmail.com>
30+
*/
31+
abstract class CompoundConstraintTestCase extends TestCase
32+
{
33+
protected ValidatorInterface $validator;
34+
protected ?ConstraintViolationListInterface $violationList = null;
35+
protected ExecutionContextInterface $context;
36+
protected string $root;
37+
38+
private mixed $validatedValue;
39+
40+
protected function setUp(): void
41+
{
42+
$this->root = 'root';
43+
$this->validator = $this->createValidator();
44+
$this->context = $this->createContext($this->validator);
45+
}
46+
47+
protected function validateValue(mixed $value): void
48+
{
49+
$this->validator->inContext($this->context)->validate($this->validatedValue = $value, $this->createCompound());
50+
}
51+
52+
protected function createValidator(): ValidatorInterface
53+
{
54+
return Validation::createValidator();
55+
}
56+
57+
protected function createContext(?ValidatorInterface $validator = null): ExecutionContextInterface
58+
{
59+
$translator = $this->createMock(TranslatorInterface::class);
60+
$translator->expects($this->any())->method('trans')->willReturnArgument(0);
61+
62+
return new ExecutionContext($validator ?? $this->createValidator(), $this->root, $translator);
63+
}
64+
65+
public function assertViolationsRaisedByCompound(Constraint|array $constraints): void
66+
{
67+
if ($constraints instanceof Constraint) {
68+
$constraints = [$constraints];
69+
}
70+
71+
$validator = new CompoundValidator();
72+
$context = $this->createContext();
73+
$validator->initialize($context);
74+
75+
$validator->validate($this->validatedValue, new class($constraints) extends Compound {
76+
public function __construct(private array $testedConstraints)
77+
{
78+
parent::__construct();
79+
}
80+
81+
protected function getConstraints(array $options): array
82+
{
83+
return $this->testedConstraints;
84+
}
85+
});
86+
87+
$expectedViolations = iterator_to_array($context->getViolations());
88+
89+
if (!$expectedViolations) {
90+
throw new ExpectationFailedException(\sprintf('Expected at least one violation for constraint(s) "%s", got none raised.', implode(', ', array_map(fn ($constraint) => $constraint::class, $constraints))));
91+
}
92+
93+
$failedToAssertViolations = [];
94+
reset($expectedViolations);
95+
foreach ($this->context->getViolations() as $violation) {
96+
if ($violation != current($expectedViolations)) {
97+
$failedToAssertViolations[] = $violation;
98+
}
99+
100+
next($expectedViolations);
101+
}
102+
103+
$this->assertEmpty(
104+
$failedToAssertViolations,
105+
\sprintf('Expected violation(s) for constraint(s) %s to be raised by compound.',
106+
implode(', ', array_map(fn ($violation) => ($violation->getConstraint())::class, $failedToAssertViolations))
107+
)
108+
);
109+
}
110+
111+
public function assertViolationsCount(int $count): void
112+
{
113+
$this->assertCount($count, $this->context->getViolations());
114+
}
115+
116+
protected function assertNoViolation(): void
117+
{
118+
$violationsCount = \count($this->context->getViolations());
119+
$this->assertSame(0, $violationsCount, \sprintf('No violation expected. Got %d.', $violationsCount));
120+
}
121+
122+
abstract protected function createCompound(): Compound;
123+
}

src/Symfony/Component/Validator/Tests/Constraints/CompoundValidatorTest.php

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@
1111

1212
namespace Symfony\Component\Validator\Tests\Constraints;
1313

14-
use Symfony\Component\Validator\Constraints\Compound;
1514
use Symfony\Component\Validator\Constraints\CompoundValidator;
16-
use Symfony\Component\Validator\Constraints\Length;
17-
use Symfony\Component\Validator\Constraints\NotBlank;
1815
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
16+
use Symfony\Component\Validator\Tests\Fixtures\DummyCompoundConstraint;
1917

2018
class CompoundValidatorTest extends ConstraintValidatorTestCase
2119
{
@@ -43,14 +41,3 @@ public function testValidateWithConstraints()
4341
$this->assertNoViolation();
4442
}
4543
}
46-
47-
class DummyCompoundConstraint extends Compound
48-
{
49-
protected function getConstraints(array $options): array
50-
{
51-
return [
52-
new NotBlank(),
53-
new Length(['max' => 3]),
54-
];
55-
}
56-
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Fixtures;
13+
14+
use Symfony\Component\Validator\Constraints\Compound;
15+
use Symfony\Component\Validator\Constraints\Length;
16+
use Symfony\Component\Validator\Constraints\NotBlank;
17+
use Symfony\Component\Validator\Constraints\Regex;
18+
19+
class DummyCompoundConstraint extends Compound
20+
{
21+
protected function getConstraints(array $options): array
22+
{
23+
return [
24+
new NotBlank(),
25+
new Length(['max' => 3]),
26+
new Regex('/[a-z]+/'),
27+
new Regex('/[0-9]+/'),
28+
];
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Fixtures;
13+
14+
use Symfony\Component\Validator\Constraints\Compound;
15+
use Symfony\Component\Validator\Constraints\Length;
16+
use Symfony\Component\Validator\Constraints\NotBlank;
17+
use Symfony\Component\Validator\Constraints\Regex;
18+
19+
class DummyCompoundConstraintWithGroups extends Compound
20+
{
21+
protected function getConstraints(array $options): array
22+
{
23+
return [
24+
new NotBlank(groups: ['not_blank']),
25+
new Length(['max' => 3], groups: ['max_length']),
26+
];
27+
}
28+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Test;
13+
14+
use PHPUnit\Framework\ExpectationFailedException;
15+
use Symfony\Component\Validator\Constraints\Compound;
16+
use Symfony\Component\Validator\Constraints\Length;
17+
use Symfony\Component\Validator\Constraints\NotBlank;
18+
use Symfony\Component\Validator\Constraints\Regex;
19+
use Symfony\Component\Validator\Test\CompoundConstraintTestCase;
20+
use Symfony\Component\Validator\Tests\Fixtures\DummyCompoundConstraint;
21+
22+
class CompoundConstraintTestCaseTest extends CompoundConstraintTestCase
23+
{
24+
protected function createCompound(): Compound
25+
{
26+
return new DummyCompoundConstraint();
27+
}
28+
29+
public function testAssertNoViolation()
30+
{
31+
$this->validateValue('ab1');
32+
33+
$this->assertNoViolation();
34+
$this->assertViolationsCount(0);
35+
}
36+
37+
public function testAssertIsRaisedByCompound()
38+
{
39+
$this->validateValue('');
40+
41+
$this->assertViolationsRaisedByCompound(new NotBlank());
42+
$this->assertViolationsCount(1);
43+
}
44+
45+
public function testMultipleAssertAreRaisedByCompound()
46+
{
47+
$this->validateValue('1245');
48+
49+
$this->assertViolationsRaisedByCompound([
50+
new Length(max: 3),
51+
new Regex('/[a-z]+/'),
52+
]);
53+
$this->assertViolationsCount(2);
54+
}
55+
56+
public function testNoAssertRaisedButExpected()
57+
{
58+
$this->validateValue('azert');
59+
60+
$this->expectException(ExpectationFailedException::class);
61+
$this->expectExceptionMessage("Expected violation(s) for constraint(s) Symfony\Component\Validator\Constraints\Length, Symfony\Component\Validator\Constraints\Regex to be raised by compound.");
62+
$this->assertViolationsRaisedByCompound([
63+
new Length(max: 5),
64+
new Regex('/^[A-Z]+$/'),
65+
]);
66+
}
67+
68+
public function testAssertRaisedByCompoundIsNotExactlyTheSame()
69+
{
70+
$this->validateValue('123');
71+
72+
$this->expectException(ExpectationFailedException::class);
73+
$this->expectExceptionMessage('Expected violation(s) for constraint(s) Symfony\Component\Validator\Constraints\Regex to be raised by compound.');
74+
$this->assertViolationsRaisedByCompound(new Regex('/^[a-z]+$/'));
75+
}
76+
77+
public function testAssertRaisedByCompoundButGotNone()
78+
{
79+
$this->validateValue('123');
80+
81+
$this->expectException(ExpectationFailedException::class);
82+
$this->expectExceptionMessage('Expected at least one violation for constraint(s) "Symfony\Component\Validator\Constraints\Length", got none raised.');
83+
$this->assertViolationsRaisedByCompound(new Length(max: 5));
84+
}
85+
}

0 commit comments

Comments
 (0)
0