test: FakeChangeLog and SchemaFixture instead of repeated mocks (T2)

- tests/Support/FakeChangeLog extends ChangeLog: records record()/recordPartial()
  calls, increasing ids, optional failure; new FakeChangeLog(id) / partialsOnly() /
  any(id) / forbidden() mirror what the replaced mocks allowed. assertRecorded() /
  assertPartials() check count and the named arguments. Calls a fake does not allow
  throw and are also verified in TestCase::tearDown (one assertion per test that
  uses a fake), so swallowed change-log calls still fail like a never() expectation.
- All ChangeLog mocks of ability/integration tests replaced (the admin tests that
  stub query/purge/clear keep their mocks); expectException tests that checked the
  partial log now catch and assert.
- SchemaFixture::managed(?divi) replaces the 21 isManaged-only ContentSchema mocks;
  DiviFixtures::schema() uses it.
Tests 1397 -> 1404 (FakeChangeLogTest); no test has fewer assertions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 17:53:54 +02:00
co-authored by Claude Opus 5.5
parent 3d96a41366
commit 72eeaa46ad
34 changed files with 725 additions and 353 deletions
+1 -6
View File
@@ -8,7 +8,6 @@ use Brain\Monkey\Functions;
use LIT\MCP\Integrations\Divi\Design\DesignSystem;
use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use Mockery;
use WP_Post;
/**
@@ -41,11 +40,7 @@ final class DiviFixtures
public static function schema(bool $diviEnabled = true): ContentSchema
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(true);
$schema->allows('diviEnabled')->andReturn($diviEnabled);
return $schema;
return SchemaFixture::managed($diviEnabled);
}
/** Shapes as GlobalData / GlobalPreset return them (Divi 5.2.1, verified in DDEV). */
+3 -7
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\ChangeLog\ChangeLog;
use LIT\MCP\Integrations\Divi\Design\DesignSystemReader;
use LIT\MCP\Integrations\Divi\Pages\DiviComposition;
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
@@ -83,13 +82,10 @@ final class DiviWriterFixture
return $saver;
}
/** A ChangeLog that accepts every record (log id 4711). */
public static function log(): ChangeLog
/** A ChangeLog that accepts records (log ids from 4711). */
public static function log(): FakeChangeLog
{
$log = Mockery::mock(ChangeLog::class);
$log->allows('record')->andReturn(4711);
return $log;
return new FakeChangeLog(4711);
}
/**
+181
View File
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\ChangeLog\ChangeLog;
use LogicException;
use PHPUnit\Framework\Assert;
use Throwable;
/**
* ChangeLog without a database. Allowed calls are kept in $records / $partials (arguments by
* name); record() returns increasing ids from $firstId, or throws $failure after noting the call.
*
* What a fake allows mirrors the Mockery mocks it replaces:
* - new FakeChangeLog(id) record() only (allows('record')->andReturn(id))
* - FakeChangeLog::partialsOnly() recordPartial() only (expects('recordPartial'))
* - FakeChangeLog::any(id) both
* - FakeChangeLog::forbidden() nothing (a mock without expectations)
* A call that is not allowed throws – and, because the code under test often swallows
* change-log failures (FollowUp), it is also noted and fails the test in TestCase::tearDown()
* via verify().
*/
class FakeChangeLog extends ChangeLog
{
/**
* @var list<array{ability: string, object_type: string, object_id: string, summary: string,
* revision_id: ?int, snapshot_id: ?string, result: string}>
*/
public array $records = [];
/**
* @var list<array{ability: string, object_type: string, object_id: string, summary: string,
* revision_id: ?int}>
*/
public array $partials = [];
/** @var list<string> Calls that were not allowed, across all fakes of the running test. */
private static array $unexpected = [];
private static bool $used = false;
public function __construct(
private int $firstId = 1,
private ?Throwable $failure = null,
private bool $allowsRecord = true,
private bool $allowsPartial = false
) {
parent::__construct();
self::$used = true;
}
public static function forbidden(): self
{
return new self(1, null, false, false);
}
public static function partialsOnly(): self
{
return new self(1, null, false, true);
}
public static function any(int $firstId = 1): self
{
return new self($firstId, null, true, true);
}
/**
* Fails the running test if any fake got a call it does not allow; resets for the next test.
* Counts as one assertion in tests that created a fake (like a Mockery never() expectation).
*/
public static function verify(): void
{
$unexpected = self::$unexpected;
$used = self::$used;
self::$unexpected = [];
self::$used = false;
if ($used || $unexpected !== []) {
Assert::assertSame([], $unexpected, 'Unexpected change log calls: ' . implode('; ', $unexpected));
}
}
public function record(
string $ability,
string $objectType,
string $objectId,
string $summary,
?int $revisionId = null,
?string $snapshotId = null,
string $result = 'ok'
): ?int {
$this->refuseUnless($this->allowsRecord, sprintf('record(%s, %s, %s)', $ability, $objectType, $objectId));
$this->records[] = [
'ability' => $ability,
'object_type' => $objectType,
'object_id' => $objectId,
'summary' => $summary,
'revision_id' => $revisionId,
'snapshot_id' => $snapshotId,
'result' => $result,
];
if ($this->failure !== null) {
throw $this->failure;
}
return $this->firstId + count($this->records) - 1;
}
public function recordPartial(
string $ability,
string $objectType,
string $objectId,
string $summary,
?int $revisionId = null
): void {
$this->refuseUnless(
$this->allowsPartial,
sprintf('recordPartial(%s, %s, %s)', $ability, $objectType, $objectId)
);
$this->partials[] = [
'ability' => $ability,
'object_type' => $objectType,
'object_id' => $objectId,
'summary' => $summary,
'revision_id' => $revisionId,
];
}
/**
* Asserts the record() calls: one $expected per call, in order; each names only the
* arguments it checks (ability, object_type, object_id, summary, revision_id, …).
*
* @param array<string, mixed> ...$expected
*/
public function assertRecorded(array ...$expected): void
{
self::assertCalls('record()', $expected, $this->records);
}
/**
* Asserts the recordPartial() calls like assertRecorded().
*
* @param array<string, mixed> ...$expected
*/
public function assertPartials(array ...$expected): void
{
self::assertCalls('recordPartial()', $expected, $this->partials);
}
/**
* @param list<array<string, mixed>> $expected
* @param list<array<string, mixed>> $actual
*/
private static function assertCalls(string $method, array $expected, array $actual): void
{
Assert::assertCount(count($expected), $actual, 'Number of ChangeLog::' . $method . ' calls');
foreach ($expected as $i => $call) {
$checked = [];
foreach (array_keys($call) as $key) {
Assert::assertArrayHasKey($key, $actual[$i], 'Unknown ChangeLog argument');
$checked[$key] = $actual[$i][$key];
}
Assert::assertSame($call, $checked, 'ChangeLog::' . $method . ' call ' . $i);
}
}
/** @return list<string> The summaries of record(), in order. */
public function summaries(): array
{
return array_column($this->records, 'summary');
}
private function refuseUnless(bool $allowed, string $call): void
{
if (! $allowed) {
self::$unexpected[] = $call;
throw new LogicException('Unexpected ChangeLog::' . $call);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Support;
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
use Mockery;
use Mockery\MockInterface;
/** ContentSchema stand-ins for ability tests. */
final class SchemaFixture
{
/**
* Every post type is managed. $divi answers diviEnabled(); null = the test does not expect
* that question (a call fails the test, as with a plain mock).
*
* @return ContentSchema&MockInterface
*/
public static function managed(?bool $divi = null): ContentSchema
{
$schema = Mockery::mock(ContentSchema::class);
$schema->allows('isManaged')->andReturn(true);
if ($divi !== null) {
$schema->allows('diviEnabled')->andReturn($divi);
}
return $schema;
}
}