Skip to content

Commit 1576365

Browse files
committed
Add documentation: unit tests of custom validator
1 parent 6c67c6e commit 1576365

File tree

2 files changed

+213
-1
lines changed

2 files changed

+213
-1
lines changed

form/unit_testing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ method is only set to ``false`` if a data transformer throws an exception::
9797

9898
Don't test the validation: it is applied by a listener that is not
9999
active in the test case and it relies on validation configuration.
100-
Instead, unit test your custom constraints directly.
100+
Instead, :ref:`unit test your custom constraints directly<testing-data-providers>`.
101101

102102
Next, verify the submission and mapping of the form. The test below
103103
checks if all the fields are correctly specified::

validation/unit_testing.rst

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
.. index::
2+
single: Validator; Custom validator testing
3+
4+
How to Unit Test your custom constraint
5+
===========================
6+
7+
.. caution::
8+
9+
This article is intended for developers who create
10+
:doc:`custom constraint </validation/custom_constraint>`. If you are using
11+
the :doc:`built-in Symfony constraints </validation>` or the constraints
12+
provided by third-party bundles, you don't need to unit test them.
13+
14+
The Validator component consists of 2 core objects while dealing with a custom validator.
15+
- a constraint (extending:class:`Symfony\\Component\\Validator\\Constraint`)
16+
- and the validator (extending:class:`Symfony\\Component\\Validator\\ConstraintValidator`).
17+
18+
.. note::
19+
20+
Depending on the way you installed your Symfony or Symfony Validator component
21+
the tests may not be downloaded. Use the ``--prefer-source`` option with
22+
Composer if this is the case.
23+
24+
The case of example
25+
----------------------------
26+
27+
The classic Order - Products example::
28+
29+
<?php
30+
31+
class Product
32+
{
33+
/** @var string */
34+
private $type;
35+
36+
public function __construct(string $type)
37+
{
38+
$this->type = $type;
39+
}
40+
41+
/**
42+
* @return string
43+
*/
44+
public function getType(): string
45+
{
46+
return $this->type;
47+
}
48+
}
49+
50+
class Order
51+
{
52+
/** @var Product[] */
53+
private $products;
54+
55+
public function __construct()
56+
{
57+
$this->products = [];
58+
}
59+
60+
public function addProduct(Product $product): void
61+
{
62+
$this->products[] = $product;
63+
}
64+
65+
public function getProducts(): array
66+
{
67+
return $this->products;
68+
}
69+
}
70+
71+
Let's imagine we want a constraint to check there is less product with same type than a specific number.
72+
73+
The Basics
74+
----------------------------
75+
76+
The constraint class
77+
**********************
78+
79+
80+
Basically your job here is to test available options of your constraint.
81+
82+
Our constraint class await a max number, so let's define it.
83+
84+
The constraint class could look like this::
85+
86+
class LimitProductTypePerOrder extends \Symfony\Component\Validator\Constraint
87+
{
88+
public $message = 'There is {{ count }} products with the type "{{ type }}", but the limit is {{ max }}.';
89+
public $max;
90+
91+
public function __construct(array $options)
92+
{
93+
parent::__construct($options);
94+
if (!is_int($this->max)) {
95+
throw new InvalidArgumentException('The max value must be an integer');
96+
}
97+
98+
if ($this->max <= 0) {
99+
throw new InvalidArgumentException('The max value must be strictly positive');
100+
}
101+
}
102+
}
103+
104+
Here you want to verify that the given options to your constraint are correct.
105+
It's mainly a variable type checking, but it could depends of your application too:
106+
::
107+
108+
class LimitProductTypePerOrderTest extends \PHPUnit\Framework\TestCase
109+
{
110+
public function testItAllowMaxInt()
111+
{
112+
$constraint = new LimitProductTypePerOrder(['max' => 1]);
113+
$this->assertEquals(1, $constraint->max);
114+
}
115+
116+
public function testItThrowIfMaxIsNotAnInt()
117+
{
118+
$this->expectException(InvalidArgumentException::class);
119+
$this->expectExceptionMessage('The max value must be an integer');
120+
new LimitProductTypePerOrder(['max' => 'abcde']);
121+
}
122+
123+
public function testItThrowIfMaxIsNegative()
124+
{
125+
$this->expectException(InvalidArgumentException::class);
126+
$this->expectExceptionMessage('The max value must be positive');
127+
new LimitProductTypePerOrder(['max' => -2]);
128+
}
129+
}
130+
131+
132+
Here you want to unit test your custom validator logic. Symfony provide a class ``ConstraintValidatorTestCase`` used internally for testing constraints available by default.
133+
This class avoid code duplication and simplify unit testing of your custom constraint.
134+
135+
It is possible to access to the validator with the ``$this->validator`` property from parent class.
136+
137+
You can use few methods to assert violations during your test
138+
139+
- ``assertNoViolation()``
140+
- ``buildViolation($constraint->message)->assertRaised();`` // Don't forget the ->assertRaised(); otherwise your tests will fail.
141+
142+
143+
144+
The Validator test class
145+
**********************
146+
In this class you will test your custom validator domain logic:
147+
::
148+
149+
150+
class LimitProductTypePerOrderValidatorTest extends ConstraintValidatorTestCase
151+
{
152+
/** @var Order|\Prophecy\Prophecy\ObjectProphecy */
153+
private $order;
154+
155+
protected function setUp(): void
156+
{
157+
parent::setUp(); // This is important
158+
$this->order = $this->prophesize(Order::class);
159+
}
160+
161+
protected function createValidator()
162+
{
163+
return new LimitProductTypePerOrderValidator();
164+
}
165+
166+
public function testItRunOnlyTheGoodConstraintType()
167+
{
168+
$randomConstraint = new \Symfony\Component\Validator\Constraint();
169+
$this->validator->validate($this->order->reveal(), $randomConstraint);
170+
171+
$this->order->getProducts()->shouldNotBeCalled();
172+
$this->assertNoViolation();
173+
}
174+
175+
public function testAddViolationIfMoreProductsWithSameTypeThanMax()
176+
{
177+
$product1 = $this->productMock('my_type');
178+
$product2 = $this->productMock('my_type');
179+
$this->order->getProducts()->willReturn([$product1, $product2]);
180+
181+
$constraint = new LimitProductTypePerOrder(['max' => 1]);
182+
$this->validator->validate($this->order->reveal(), $constraint);
183+
184+
$this->buildViolation($constraint->message)
185+
->setParameter('{{ max }}', 1)
186+
->setParameter('{{ count }}', 2)
187+
->setParameter('{{ type }}', 'my_type')
188+
->assertRaised();
189+
}
190+
191+
public function testItDontAddViolation()
192+
{
193+
$product1 = $this->productMock('symfony');
194+
$product2 = $this->productMock('is');
195+
$product3 = $this->productMock('awesome');
196+
$product4 = $this->productMock('!');
197+
$this->order->getProducts()->willReturn([$product1, $product2, $product3, $product4]);
198+
199+
$constraint = new LimitProductTypePerOrder(['max' => 1]);
200+
$this->validator->validate($this->order->reveal(), $constraint);
201+
202+
$this->assertNoViolation();
203+
}
204+
205+
private function productMock(string $type)
206+
{
207+
$productMock = $this->prophesize(Product::class);
208+
$productMock->getType()->willReturn($type);
209+
return $productMock->reveal();
210+
}
211+
}
212+

0 commit comments

Comments
 (0)