Skip to content

Commit 8983f53

Browse files
Stephan Wentzpl-github
authored andcommitted
feat: Add uuid trait
1 parent 47d7343 commit 8983f53

File tree

5 files changed

+336
-1
lines changed

5 files changed

+336
-1
lines changed

README.uuid-trait.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# brainbits Functional Test Helpers
2+
3+
## Uuid Trait
4+
5+
To use the uuid helpers, use the `UuidTrait` in your test class.
6+
7+
The uuid trait provides several methods for handling UUIDs in tests.
8+
9+
### nextUuid()
10+
11+
This method always provides predictable UUIDs on subsequent calls.
12+
13+
```php
14+
self::nextUuid(); # 00000000-0000-0000-0000-000000000001
15+
self::nextUuid(); # 00000000-0000-0000-0000-000000000002
16+
self::nextUuid(); # 00000000-0000-0000-0000-000000000003
17+
```
18+
19+
### assertIsUuid()
20+
21+
This assertion fails on invalid UUIDs.
22+
23+
```php
24+
self::assertIsUuid('6b471a56-faf2-11ed-ac67-afd829470994'); # success
25+
self::assertIsUuid('foo'); # failure
26+
```
27+
28+
### assertAndReplaceUuidInJson()
29+
30+
This assertion validates a UUID in a simple array structure, and replaces the UUID
31+
with a predictable value. Recommended for example in combination with snapshot tests.
32+
33+
```php
34+
$data = '{"id": "6b471a56-faf2-11ed-ac67-afd829470994", "name": "test"}';
35+
36+
$predictableData = self::assertAndReplaceUuidInJson($data, 'id');
37+
38+
// $predictableData = '{"id": "00000000-0000-0000-0000-000000000001", "name": "test"}';
39+
```
40+
41+
### assertAndReplaceUuidInArray()
42+
43+
This assertion validates a UUID in a simple array structure, and replaces the UUID
44+
with a predictable value. Recommended for example in combination with snapshot tests.
45+
46+
```php
47+
$data = [
48+
'id' => '6b471a56-faf2-11ed-ac67-afd829470994',
49+
'name' => 'test',
50+
];
51+
52+
$predictableData = self::assertAndReplaceUuidInArray($data, 'id');
53+
54+
// $predictableData = [
55+
// 'id' => '00000000-0000-0000-0000-000000000001',
56+
// 'name' => 'test',
57+
// ];
58+
```

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"symfony/mime": "^6.2",
4141
"symfony/security-core": "^5.3|^6.0",
4242
"symfony/security-csrf": "^6.2",
43+
"symfony/uid": "^6.2",
4344
"thecodingmachine/phpstan-safe-rule": "^1.1",
4445
"thecodingmachine/phpstan-strict-rules": "^1.0"
4546
},
@@ -61,7 +62,8 @@
6162
"symfony/http-client": "For http client mock trait",
6263
"symfony/http-foundation": "For request trait",
6364
"symfony/framework-bundle": "For request trait, when using authLogin(). Requires version >= 5.1",
64-
"symfony/security-core": "For request trait"
65+
"symfony/security-core": "For request trait",
66+
"symfony/uid": "For uuid trait"
6567
},
6668
"config": {
6769
"preferred-install": {

phpstan.neon.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ parameters:
1313
# - '#Class DateTimeImmutable is unsafe to use#' # reactivate after updating to thecodingmachine/safe >= 2.0
1414
- '#Constructor in .* has parameter .* with default value#'
1515
- '#In method "Brainbits\\FunctionalTestHelpers\\Snapshot\\IsXml::.*", caught "Throwable" must be rethrown. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception.#'
16+
- '#In method "Brainbits\\FunctionalTestHelpers\\Tests\\Uuid\\UuidTraitTest::.*", caught "Throwable" must be rethrown. Either catch a more specific exception or add a "throw" clause in the "catch" block to propagate the exception.#'
1617
#- '#Method .* has a nullable return type declaration#'
1718
#- '#Method .* has parameter .* with a nullable type declaration#'
1819
- '#Method .* has parameter .* with null as default value#'
1920
- '#Method Brainbits\\FunctionalTestHelpers\\Tests\\.*Test::assertMatches.*Snapshot\(\) is protected, but since the containing class is final, it can be private.#'
2021
- '#Method Brainbits\\FunctionalTestHelpers\\Tests\\.*Test::setUpSnapshot\(\) is protected, but since the containing class is final, it can be private.#'
2122
- '#Method Brainbits\\FunctionalTestHelpers\\Tests\\Request\\RequestTraitTest::.*\(\) is protected, but since the containing class is final, it can be private#'
23+
- '#Method Brainbits\\FunctionalTestHelpers\\Tests\\Uuid\\UuidTraitTest::.*\(\) is protected, but since the containing class is final, it can be private#'
2224
- '#Method Brainbits\\FunctionalTestHelpers\\Tests\\ZipContents\\ZipContentsTraitTest::.*\(\) is protected, but since the containing class is final, it can be private#'
2325
ergebnis:
2426
classesAllowedToBeExtended:

src/Uuid/UuidTrait.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brainbits\FunctionalTestHelpers\Uuid;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Symfony\Component\Uid\NilUuid;
9+
use Symfony\Component\Uid\Uuid;
10+
11+
use function dechex;
12+
use function Safe\json_decode;
13+
use function Safe\json_encode;
14+
use function str_pad;
15+
use function substr_replace;
16+
17+
use const STR_PAD_LEFT;
18+
19+
/** @mixin TestCase */
20+
trait UuidTrait
21+
{
22+
private int $lastUuidValue;
23+
24+
/** @before */
25+
final protected function setUpUuidTrait(): void
26+
{
27+
$this->lastUuidValue = 0;
28+
}
29+
30+
final protected function nextUuid(): string
31+
{
32+
$this->lastUuidValue ??= 0;
33+
34+
$uuid = str_pad(dechex(++$this->lastUuidValue), 32, '0', STR_PAD_LEFT);
35+
$uuid = substr_replace($uuid, '-', 8, 0);
36+
$uuid = substr_replace($uuid, '-', 13, 0);
37+
$uuid = substr_replace($uuid, '-', 18, 0);
38+
$uuid = substr_replace($uuid, '-', 23, 0);
39+
40+
return (string) Uuid::fromString($uuid);
41+
}
42+
43+
/** @param string $actual */
44+
final protected static function assertIsUuid($actual, string $message = ''): void // phpcs:ignore
45+
{
46+
self::assertIsString($actual, $message);
47+
self::assertTrue(Uuid::isValid($actual), $message);
48+
}
49+
50+
/** @param string $jsonData */
51+
final protected static function assertAndReplaceUuidInJson($jsonData, string $key): string // phpcs:ignore
52+
{
53+
self::assertJson($jsonData);
54+
55+
$jsonData = self::assertAndReplaceUuidInArray(json_decode($jsonData, true), $key);
56+
57+
return json_encode($jsonData);
58+
}
59+
60+
/**
61+
* @param mixed[] $arrayData
62+
*
63+
* @return mixed[]
64+
*/
65+
final protected static function assertAndReplaceUuidInArray($arrayData, string $key): array // phpcs:ignore
66+
{
67+
self::assertIsArray($arrayData);
68+
69+
if (($arrayData[$key] ?? null) !== null) {
70+
self::assertIsUuid($arrayData[$key] ?? null);
71+
72+
$arrayData[$key] = (string) (new NilUuid());
73+
}
74+
75+
return $arrayData;
76+
}
77+
}

tests/Uuid/UuidTraitTest.php

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Brainbits\FunctionalTestHelpers\Tests\Uuid;
6+
7+
use Brainbits\FunctionalTestHelpers\Uuid\UuidTrait;
8+
use PHPUnit\Framework\ExpectationFailedException;
9+
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\Uid\Uuid;
11+
use Throwable;
12+
13+
use function Safe\json_encode;
14+
15+
/** @covers \Brainbits\FunctionalTestHelpers\Uuid\UuidTrait */
16+
final class UuidTraitTest extends TestCase
17+
{
18+
use UuidTrait;
19+
20+
public function testNextUuid(): void
21+
{
22+
self::assertEquals('00000000-0000-0000-0000-000000000001', $this->nextUuid());
23+
self::assertEquals('00000000-0000-0000-0000-000000000002', $this->nextUuid());
24+
self::assertEquals('00000000-0000-0000-0000-000000000003', $this->nextUuid());
25+
self::assertEquals('00000000-0000-0000-0000-000000000004', $this->nextUuid());
26+
self::assertEquals('00000000-0000-0000-0000-000000000005', $this->nextUuid());
27+
self::assertEquals('00000000-0000-0000-0000-000000000006', $this->nextUuid());
28+
self::assertEquals('00000000-0000-0000-0000-000000000007', $this->nextUuid());
29+
self::assertEquals('00000000-0000-0000-0000-000000000008', $this->nextUuid());
30+
self::assertEquals('00000000-0000-0000-0000-000000000009', $this->nextUuid());
31+
self::assertEquals('00000000-0000-0000-0000-00000000000a', $this->nextUuid());
32+
self::assertEquals('00000000-0000-0000-0000-00000000000b', $this->nextUuid());
33+
self::assertEquals('00000000-0000-0000-0000-00000000000c', $this->nextUuid());
34+
self::assertEquals('00000000-0000-0000-0000-00000000000d', $this->nextUuid());
35+
self::assertEquals('00000000-0000-0000-0000-00000000000e', $this->nextUuid());
36+
self::assertEquals('00000000-0000-0000-0000-00000000000f', $this->nextUuid());
37+
self::assertEquals('00000000-0000-0000-0000-000000000010', $this->nextUuid());
38+
}
39+
40+
public function testAssertIsUuid(): void
41+
{
42+
$uuid = (string) Uuid::v4();
43+
$noUuid = 'foo';
44+
45+
self::assertIsUuid($uuid);
46+
47+
try {
48+
self::assertIsUuid($noUuid);
49+
50+
self::fail('Expected ExpectationFailedException exception was not thrown');
51+
} catch (ExpectationFailedException) {
52+
// @ignoreException
53+
} catch (Throwable) {
54+
self::fail('Expected ExpectationFailedException exception was not thrown');
55+
}
56+
}
57+
58+
public function testAssertAndReplaceUuidInJson(): void
59+
{
60+
$json = json_encode([
61+
'foo' => (string) Uuid::v4(),
62+
'bar' => 'baz',
63+
]);
64+
65+
$replacedJson = self::assertAndReplaceUuidInJson($json, 'foo');
66+
67+
self::assertJson($replacedJson);
68+
self::assertJsonStringEqualsJsonString(
69+
'{"foo":"00000000-0000-0000-0000-000000000000","bar":"baz"}',
70+
$replacedJson,
71+
);
72+
}
73+
74+
public function testAssertAndReplaceUuidInJsonWithNullUuidValueDoesNotReplaceUuid(): void
75+
{
76+
$json = json_encode([
77+
'foo' => null,
78+
'bar' => 'baz',
79+
]);
80+
81+
$replacedJson = self::assertAndReplaceUuidInJson($json, 'foo');
82+
83+
self::assertJson($replacedJson);
84+
self::assertJsonStringEqualsJsonString(
85+
'{"foo":null,"bar":"baz"}',
86+
$replacedJson,
87+
);
88+
}
89+
90+
public function testAssertAndReplaceUuidInJsonFailsOnInvalidJson(): void
91+
{
92+
$json = 'foo';
93+
94+
try {
95+
self::assertAndReplaceUuidInJson($json, 'foo');
96+
97+
self::fail('Expected ExpectationFailedException exception was not thrown');
98+
} catch (ExpectationFailedException $e) {
99+
self::assertSame(
100+
'Failed asserting that \'foo\' is valid JSON (Syntax error, malformed JSON).',
101+
$e->getMessage(),
102+
);
103+
} catch (Throwable) {
104+
self::fail('Expected ExpectationFailedException exception was not thrown');
105+
}
106+
}
107+
108+
public function testAssertAndReplaceUuidInJsonFailsOnInvalidUuid(): void
109+
{
110+
$json = json_encode([
111+
'foo' => 'xxx',
112+
'bar' => 'baz',
113+
]);
114+
115+
try {
116+
self::assertAndReplaceUuidInJson($json, 'foo');
117+
118+
self::fail('Expected ExpectationFailedException exception was not thrown');
119+
} catch (ExpectationFailedException $e) {
120+
self::assertSame(
121+
'Failed asserting that false is true.',
122+
$e->getMessage(),
123+
);
124+
} catch (Throwable) {
125+
self::fail('Expected ExpectationFailedException exception was not thrown');
126+
}
127+
}
128+
129+
public function testAssertAndReplaceUuidInArray(): void
130+
{
131+
$data = [
132+
'foo' => (string) Uuid::v4(),
133+
'bar' => 'baz',
134+
];
135+
136+
$replacedData = self::assertAndReplaceUuidInArray($data, 'foo');
137+
138+
self::assertIsArray($replacedData);
139+
self::assertSame(
140+
['foo' => '00000000-0000-0000-0000-000000000000', 'bar' => 'baz'],
141+
$replacedData,
142+
);
143+
}
144+
145+
public function testAssertAndReplaceUuidInArrayWithNullUuidValueDoesNotReplaceUuid(): void
146+
{
147+
$data = [
148+
'foo' => null,
149+
'bar' => 'baz',
150+
];
151+
152+
$replacedData = self::assertAndReplaceUuidInArray($data, 'foo');
153+
154+
self::assertIsArray($replacedData);
155+
self::assertSame(
156+
['foo' => null, 'bar' => 'baz'],
157+
$replacedData,
158+
);
159+
}
160+
161+
public function testAssertAndReplaceUuidInArrayFailsOnInvalidJson(): void
162+
{
163+
$data = 'foo';
164+
165+
try {
166+
self::assertAndReplaceUuidInArray($data, 'foo');
167+
168+
self::fail('Expected ExpectationFailedException exception was not thrown');
169+
} catch (ExpectationFailedException $e) {
170+
self::assertSame('Failed asserting that \'foo\' is of type array.', $e->getMessage());
171+
} catch (Throwable) {
172+
self::fail('Expected ExpectationFailedException exception was not thrown');
173+
}
174+
}
175+
176+
public function testAssertAndReplaceUuidInArrayFailsOnInvalidUuid(): void
177+
{
178+
$data = [
179+
'foo' => 'xxx',
180+
'bar' => 'baz',
181+
];
182+
183+
try {
184+
self::assertAndReplaceUuidInArray($data, 'foo');
185+
186+
self::fail('Expected ExpectationFailedException exception was not thrown');
187+
} catch (ExpectationFailedException $e) {
188+
self::assertSame(
189+
'Failed asserting that false is true.',
190+
$e->getMessage(),
191+
);
192+
} catch (Throwable) {
193+
self::fail('Expected ExpectationFailedException exception was not thrown');
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)