Skip to content

Commit 4b2cb6d

Browse files
dergelclaude
andcommitted
Konsolen-basierte Test-Suite (Phase 0)
11 Suiten via `php redaxo/bin/console yform:test`: tables, table-read, fields, datasets, queries, validators, actions, extension-points, authorization, cache, field-types. 132 Tests grün, 6 als known-issue markiert (siehe tests/README.md). Kein PHPUnit-Dependency — eigene Runner-/Assertion-Library unter lib/Test/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c60f98d commit 4b2cb6d

41 files changed

Lines changed: 5923 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
vendor/
33
.phpunit.result.cache
44
.php-cs-fixer.cache
5+
.claude/

CLAUDE.md

Lines changed: 229 additions & 0 deletions
Large diffs are not rendered by default.

lib/Test/AbstractTestSuite.php

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Redaxo\YForm\Test;
6+
7+
use Countable;
8+
use Redaxo\YForm\Test\Exception\TestSkippedException;
9+
use rex_yform_manager_table;
10+
use Throwable;
11+
12+
/**
13+
* Base class for test suites. Subclass and add public test* methods.
14+
*
15+
* Lifecycle per method:
16+
* setUp() -> testFoo() -> tearDown()
17+
*
18+
* Lifecycle per suite:
19+
* setUpBeforeClass() -> [per-method cycle, repeated] -> tearDownAfterClass()
20+
*
21+
* @package redaxo\yform
22+
* @internal
23+
*/
24+
abstract class AbstractTestSuite
25+
{
26+
public function __construct(
27+
protected readonly FixtureManager $fixtures,
28+
protected readonly MailerStub $mailer,
29+
) {}
30+
31+
/**
32+
* Override to run setup before each test* method.
33+
*/
34+
public function setUp(): void {}
35+
36+
/**
37+
* Override to run teardown after each test* method.
38+
*/
39+
public function tearDown(): void {}
40+
41+
/**
42+
* Override to run setup once before any test* method.
43+
*/
44+
public function setUpBeforeClass(): void {}
45+
46+
/**
47+
* Override to run teardown once after all test* methods.
48+
*/
49+
public function tearDownAfterClass(): void {}
50+
51+
/** Human-readable name. Override for custom titles. */
52+
public function getSuiteTitle(): string
53+
{
54+
$short = (new \ReflectionClass(static::class))->getShortName();
55+
return preg_replace('/Suite$/', '', $short) ?: $short;
56+
}
57+
58+
// ---------- Assertion shortcuts ----------
59+
60+
protected function assertSame(mixed $expected, mixed $actual, string $msg = ''): void
61+
{
62+
Assert::same($expected, $actual, $msg);
63+
}
64+
65+
protected function assertNotSame(mixed $unexpected, mixed $actual, string $msg = ''): void
66+
{
67+
Assert::notSame($unexpected, $actual, $msg);
68+
}
69+
70+
protected function assertEquals(mixed $expected, mixed $actual, string $msg = ''): void
71+
{
72+
Assert::equals($expected, $actual, $msg);
73+
}
74+
75+
protected function assertTrue(bool $cond, string $msg = ''): void
76+
{
77+
Assert::true($cond, $msg);
78+
}
79+
80+
protected function assertFalse(bool $cond, string $msg = ''): void
81+
{
82+
Assert::false($cond, $msg);
83+
}
84+
85+
protected function assertNull(mixed $value, string $msg = ''): void
86+
{
87+
Assert::null($value, $msg);
88+
}
89+
90+
protected function assertNotNull(mixed $value, string $msg = ''): void
91+
{
92+
Assert::notNull($value, $msg);
93+
}
94+
95+
protected function assertCount(int $expected, Countable|array $actual, string $msg = ''): void
96+
{
97+
Assert::count($expected, $actual, $msg);
98+
}
99+
100+
/**
101+
* @param class-string $class
102+
*/
103+
protected function assertInstanceOf(string $class, mixed $actual, string $msg = ''): void
104+
{
105+
Assert::instanceOf($class, $actual, $msg);
106+
}
107+
108+
/**
109+
* @param class-string<Throwable> $exceptionClass
110+
*/
111+
protected function assertThrows(string $exceptionClass, callable $fn, string $msg = ''): void
112+
{
113+
Assert::throws($exceptionClass, $fn, $msg);
114+
}
115+
116+
protected function assertStringContains(string $needle, string $haystack, string $msg = ''): void
117+
{
118+
Assert::stringContains($needle, $haystack, $msg);
119+
}
120+
121+
protected function assertArrayHasKey(int|string $key, array $array, string $msg = ''): void
122+
{
123+
Assert::arrayHasKey($key, $array, $msg);
124+
}
125+
126+
protected function assertArrayNotHasKey(int|string $key, array $array, string $msg = ''): void
127+
{
128+
Assert::arrayNotHasKey($key, $array, $msg);
129+
}
130+
131+
/**
132+
* Aborts the current test method as "skipped" (not a failure).
133+
*/
134+
protected function markSkipped(string $reason): never
135+
{
136+
throw new TestSkippedException($reason);
137+
}
138+
139+
// ---------- Fixture shortcuts ----------
140+
141+
/**
142+
* @param array<int, array<string, mixed>> $fields
143+
*/
144+
protected function createTestTable(string $shortName, array $fields = [], array $tableOptions = []): rex_yform_manager_table
145+
{
146+
return $this->fixtures->createTable($shortName, $fields, $tableOptions);
147+
}
148+
149+
protected function loadFixture(string $relativePath): rex_yform_manager_table
150+
{
151+
return $this->fixtures->loadFromJson($relativePath);
152+
}
153+
}

lib/Test/Assert.php

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Redaxo\YForm\Test;
6+
7+
use Countable;
8+
use Redaxo\YForm\Test\Exception\AssertionFailedException;
9+
use Throwable;
10+
11+
/**
12+
* In-house assertion library used by AbstractTestSuite.
13+
*
14+
* Keeps PHPUnit out of the runtime path so the test commands can run on any
15+
* REDAXO install without a dev dependency.
16+
*
17+
* @package redaxo\yform
18+
* @internal
19+
*/
20+
final class Assert
21+
{
22+
public static function same(mixed $expected, mixed $actual, string $msg = ''): void
23+
{
24+
if ($expected !== $actual) {
25+
throw new AssertionFailedException(
26+
($msg ?: 'Failed asserting two values are identical.')
27+
. "\n expected: " . self::dump($expected)
28+
. "\n actual: " . self::dump($actual),
29+
);
30+
}
31+
}
32+
33+
public static function notSame(mixed $unexpected, mixed $actual, string $msg = ''): void
34+
{
35+
if ($unexpected === $actual) {
36+
throw new AssertionFailedException(
37+
($msg ?: 'Failed asserting two values are not identical.')
38+
. "\n value: " . self::dump($actual),
39+
);
40+
}
41+
}
42+
43+
public static function equals(mixed $expected, mixed $actual, string $msg = ''): void
44+
{
45+
if ($expected != $actual) {
46+
throw new AssertionFailedException(
47+
($msg ?: 'Failed asserting two values are equal.')
48+
. "\n expected: " . self::dump($expected)
49+
. "\n actual: " . self::dump($actual),
50+
);
51+
}
52+
}
53+
54+
public static function true(bool $cond, string $msg = ''): void
55+
{
56+
if (!$cond) {
57+
throw new AssertionFailedException($msg ?: 'Failed asserting that condition is true.');
58+
}
59+
}
60+
61+
public static function false(bool $cond, string $msg = ''): void
62+
{
63+
if ($cond) {
64+
throw new AssertionFailedException($msg ?: 'Failed asserting that condition is false.');
65+
}
66+
}
67+
68+
public static function null(mixed $value, string $msg = ''): void
69+
{
70+
if (null !== $value) {
71+
throw new AssertionFailedException(
72+
($msg ?: 'Failed asserting null.') . "\n actual: " . self::dump($value),
73+
);
74+
}
75+
}
76+
77+
public static function notNull(mixed $value, string $msg = ''): void
78+
{
79+
if (null === $value) {
80+
throw new AssertionFailedException($msg ?: 'Failed asserting not-null.');
81+
}
82+
}
83+
84+
public static function count(int $expected, Countable|array $actual, string $msg = ''): void
85+
{
86+
$c = is_array($actual) ? count($actual) : $actual->count();
87+
if ($c !== $expected) {
88+
throw new AssertionFailedException(
89+
($msg ?: 'Failed asserting count.') . " expected={$expected} actual={$c}",
90+
);
91+
}
92+
}
93+
94+
/**
95+
* @param class-string $class
96+
*/
97+
public static function instanceOf(string $class, mixed $actual, string $msg = ''): void
98+
{
99+
if (!($actual instanceof $class)) {
100+
$got = is_object($actual) ? $actual::class : gettype($actual);
101+
throw new AssertionFailedException(
102+
($msg ?: 'Failed asserting instance.') . " expected={$class} got={$got}",
103+
);
104+
}
105+
}
106+
107+
/**
108+
* @param class-string<Throwable> $exceptionClass
109+
*/
110+
public static function throws(string $exceptionClass, callable $fn, string $msg = ''): void
111+
{
112+
try {
113+
$fn();
114+
} catch (Throwable $e) {
115+
if (!($e instanceof $exceptionClass)) {
116+
throw new AssertionFailedException(
117+
($msg ?: 'Wrong exception type.')
118+
. " expected={$exceptionClass} got=" . $e::class
119+
. "\n message: " . $e->getMessage(),
120+
);
121+
}
122+
return;
123+
}
124+
throw new AssertionFailedException(
125+
($msg ?: 'No exception was thrown.') . " expected={$exceptionClass}",
126+
);
127+
}
128+
129+
public static function stringContains(string $needle, string $haystack, string $msg = ''): void
130+
{
131+
if (!str_contains($haystack, $needle)) {
132+
throw new AssertionFailedException(
133+
($msg ?: 'Failed asserting string contains.')
134+
. "\n needle: " . self::dump($needle)
135+
. "\n haystack: " . self::dump($haystack),
136+
);
137+
}
138+
}
139+
140+
public static function arrayHasKey(int|string $key, array $array, string $msg = ''): void
141+
{
142+
if (!array_key_exists($key, $array)) {
143+
throw new AssertionFailedException(
144+
($msg ?: 'Failed asserting array has key.') . " key=" . self::dump($key),
145+
);
146+
}
147+
}
148+
149+
public static function arrayNotHasKey(int|string $key, array $array, string $msg = ''): void
150+
{
151+
if (array_key_exists($key, $array)) {
152+
throw new AssertionFailedException(
153+
($msg ?: 'Failed asserting array does not have key.') . " key=" . self::dump($key),
154+
);
155+
}
156+
}
157+
158+
/**
159+
* Truncated, single-line representation for error messages.
160+
*/
161+
private static function dump(mixed $value): string
162+
{
163+
if (is_string($value)) {
164+
$v = mb_strlen($value) > 80 ? mb_substr($value, 0, 77) . '...' : $value;
165+
return '"' . $v . '"';
166+
}
167+
if (is_array($value)) {
168+
$json = (string) json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
169+
$json = mb_strlen($json) > 120 ? mb_substr($json, 0, 117) . '...' : $json;
170+
return '[' . count($value) . '] ' . $json;
171+
}
172+
if (is_object($value)) {
173+
return $value::class . '#' . spl_object_id($value);
174+
}
175+
if (is_bool($value)) {
176+
return $value ? 'true' : 'false';
177+
}
178+
if (null === $value) {
179+
return 'null';
180+
}
181+
return var_export($value, true);
182+
}
183+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Redaxo\YForm\Test\Exception;
6+
7+
use RuntimeException;
8+
9+
/**
10+
* Thrown by Assert::* when a test assertion fails.
11+
*
12+
* @package redaxo\yform
13+
* @internal
14+
*/
15+
final class AssertionFailedException extends RuntimeException {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Redaxo\YForm\Test\Exception;
6+
7+
use RuntimeException;
8+
9+
/**
10+
* Thrown by FixtureManager when a fixture cannot be created or loaded.
11+
*
12+
* @package redaxo\yform
13+
* @internal
14+
*/
15+
final class FixtureException extends RuntimeException {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Redaxo\YForm\Test\Exception;
6+
7+
use RuntimeException;
8+
9+
/**
10+
* Thrown by AbstractTestSuite::markSkipped() to abort the current test method
11+
* without flagging it as a failure.
12+
*
13+
* @package redaxo\yform
14+
* @internal
15+
*/
16+
final class TestSkippedException extends RuntimeException {}

0 commit comments

Comments
 (0)