foundation/tests/Unit/Core/Database/UnitOfWorkTest.php
2025-06-13 18:29:55 +02:00

306 lines
No EOL
11 KiB
PHP

<?php
declare(strict_types = 1);
namespace Foundation\Tests\Unit\Core\Database;
use Foundation\Core\Database\EntityPersisterInterface;
use Foundation\Core\Database\EntityState;
use Foundation\Core\Database\UnitOfWork;
use PDO;
use PDOException;
use PHPUnit\Framework\TestCase;
use RuntimeException;
class UnitOfWorkTest extends TestCase
{
private PDO $pdo;
private UnitOfWork $unitOfWork;
protected function setUp(): void {
$this->pdo = $this->createMock(PDO::class);
$this->unitOfWork = new UnitOfWork($this->pdo);
}
public function testSetsEntityStateToNewWhenRegisteringANewEntity(): void {
$entity = new TestEntity();
$this->unitOfWork->registerNew($entity);
self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($entity));
}
public function testSetsEntityStateToDirtyWhenRegisteringADirtyEntity(): void {
$entity = new TestEntity();
$this->unitOfWork->registerDirty($entity);
self::assertSame(EntityState::DIRTY, $this->unitOfWork->getEntityState($entity));
}
public function testDoesNotOverrideNewStateWhenRegisteringADirtyEntity(): void {
$entity = new TestEntity();
$this->unitOfWork->registerNew($entity);
$this->unitOfWork->registerDirty($entity);
self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($entity));
}
public function testSetsEntityStateToRemovedWhenRegisteringARemovedEntity(): void {
$entity = new TestEntity();
$this->unitOfWork->registerRemoved($entity);
self::assertSame(EntityState::REMOVED, $this->unitOfWork->getEntityState($entity));
}
public function testSetsEntityStateToCleanWhenRegisteringCleanEntity(): void {
$entity = new TestEntity();
$this->unitOfWork->registerDirty($entity);
$this->unitOfWork->registerClean($entity);
self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($entity));
}
public function testReturnsCleanStateForUnknownEntities(): void {
$entity = new TestEntity();
self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($entity));
}
public function testDoesNotStartTransactionWhenCommittingWithNoChanges(): void {
$this->pdo->expects(self::never())->method('beginTransaction');
$this->unitOfWork->commit();
}
public function testDoesStartTransactionAndCommitIt(): void {
$entity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($entity);
$persister->expects(self::once())->method('supports')->with($entity)->willReturn(true);
$persister->expects(self::once())->method('insert')->with($entity, $this->pdo);
$this->pdo->expects(self::once())->method('beginTransaction');
$this->pdo->expects(self::once())->method('commit');
$this->unitOfWork->commit();
}
public function testProcessesEntitiesInCorrectOrderOnCommit(): void {
$newEntity = new TestEntity();
$dirtyEntity = new TestEntity();
$removedEntity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($newEntity);
$this->unitOfWork->registerDirty($dirtyEntity);
$this->unitOfWork->registerRemoved($removedEntity);
$callOrder = [];
$persister->method('supports')->willReturn(true);
$persister->method('delete')->willReturnCallback(
function () use (&$callOrder): void {
$callOrder[] = 'delete';
},
);
$persister->method('insert')->willReturnCallback(
function () use (&$callOrder): void {
$callOrder[] = 'insert';
},
);
$persister->method('update')->willReturnCallback(
function () use (&$callOrder): void {
$callOrder[] = 'update';
},
);
$this->pdo->method('beginTransaction');
$this->pdo->method('commit');
$this->unitOfWork->commit();
self::assertSame(['delete', 'insert', 'update'], $callOrder);
}
public function testMarksAllEntitiesAsCleanAfterASuccessfulCommit(): void {
$newEntity = new TestEntity();
$dirtyEntity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$persister->method('supports')->willReturn(true);
$persister->method('insert');
$persister->method('update');
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($newEntity);
$this->unitOfWork->registerDirty($dirtyEntity);
$this->pdo->method('beginTransaction');
$this->pdo->method('commit');
$this->unitOfWork->commit();
self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($newEntity));
self::assertSame(EntityState::CLEAN, $this->unitOfWork->getEntityState($dirtyEntity));
}
public function testClearsCollectionsAfterASuccessfulCommit(): void {
$entity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$persister->method('supports')->willReturn(true);
$persister->method('insert');
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($entity);
$this->pdo->method('beginTransaction');
$this->pdo->method('commit');
$this->unitOfWork->commit();
// Try to register the same entity again - should work if collections are cleared
$this->unitOfWork->registerNew($entity);
self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($entity));
}
public function testRollsBackOnPDOException(): void {
$entity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$persister->method('supports')->willReturn(true);
$persister->method('insert')->willThrowException(new PDOException('Database error'));
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($entity);
$this->pdo->expects(self::once())->method('beginTransaction');
$this->pdo->expects(self::once())->method('rollBack');
$this->pdo->expects(self::never())->method('commit');
$this->expectException(PDOException::class);
$this->unitOfWork->commit();
}
public function testCallsPdoRollbackWhenInTransaction(): void {
$entity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$persister->method('supports')->willReturn(true);
$persister->method('insert')->willThrowException(new PDOException('Database error'));
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($entity);
$this->pdo->method('beginTransaction')->willReturn(true);
$this->pdo->expects(self::once())->method('rollBack');
try {
$this->unitOfWork->commit();
} catch (PDOException $e) {
// Expected exception
}
self::assertFalse($this->unitOfWork->isInTransaction());
}
public function testDoesNothingWhenNotInTransactionOnRollback(): void {
$this->pdo->expects(self::never())->method('rollBack');
$this->unitOfWork->rollback();
}
public function testCanClearInternalState(): void {
$newEntity = new TestEntity();
$dirtyEntity = new TestEntity();
$removedEntity = new TestEntity();
$this->unitOfWork->registerNew($newEntity);
$this->unitOfWork->registerDirty($dirtyEntity);
$this->unitOfWork->registerRemoved($removedEntity);
$this->unitOfWork->clear();
// After clear, registering the same entities should work
$this->unitOfWork->registerNew($newEntity);
$this->unitOfWork->registerDirty($dirtyEntity);
$this->unitOfWork->registerRemoved($removedEntity);
self::assertSame(EntityState::NEW, $this->unitOfWork->getEntityState($newEntity));
self::assertSame(EntityState::DIRTY, $this->unitOfWork->getEntityState($dirtyEntity));
self::assertSame(EntityState::REMOVED, $this->unitOfWork->getEntityState($removedEntity));
}
public function testCallsPersisterOnEntityPersistence(): void {
$entity = new TestEntity();
$persister = $this->createMock(EntityPersisterInterface::class);
$persister->expects(self::once())->method('supports')->with($entity)->willReturn(true);
$persister->expects(self::once())->method('insert')->with($entity, $this->pdo);
$this->unitOfWork->registerPersister($persister);
$this->unitOfWork->registerNew($entity);
$this->pdo->method('beginTransaction');
$this->pdo->method('commit');
$this->unitOfWork->commit();
}
public function testThrowsRuntimeExceptionWhenNoPersisterWasFound(): void {
$entity = new TestEntity();
$this->unitOfWork->registerNew($entity);
$this->pdo->method('beginTransaction');
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('No persister found for entity of type');
$this->unitOfWork->commit();
}
public function testCanHandleMultipleRegisteredPersisters(): void {
$entity1 = new TestEntity();
$entity2 = new AnotherTestEntity();
$persister1 = $this->createMock(EntityPersisterInterface::class);
$persister2 = $this->createMock(EntityPersisterInterface::class);
$persister1->method('supports')->willReturnCallback(fn($entity) => $entity instanceof TestEntity);
$persister2->method('supports')->willReturnCallback(fn($entity) => $entity instanceof AnotherTestEntity);
$persister1->expects(self::once())->method('insert')->with($entity1, $this->pdo);
$persister2->expects(self::once())->method('insert')->with($entity2, $this->pdo);
$this->unitOfWork->registerPersister($persister1);
$this->unitOfWork->registerPersister($persister2);
$this->unitOfWork->registerNew($entity1);
$this->unitOfWork->registerNew($entity2);
$this->pdo->method('beginTransaction');
$this->pdo->method('commit');
$this->unitOfWork->commit();
}
}
class TestEntity
{
public function __construct(public string $id = 'test-id') {}
}
class AnotherTestEntity
{
public function __construct(public string $id = 'another-test-id') {}
}