306 lines
No EOL
11 KiB
PHP
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') {}
|
|
} |