-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add integration tests and enhance CI workflow for multiple platforms
Add integration tests with SQLite, MySQL, PostgreSQL. Run SQLite tests on Windows and Linux. Run MySQL/PostgreSQL tests on Linux.
- Loading branch information
Showing
6 changed files
with
391 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,42 +5,125 @@ on: | |
pull_request: | ||
|
||
jobs: | ||
phpunit: | ||
name: PHPUnit with PHP ${{ matrix.php }} and Symfony ${{ matrix.symfony }} and Doctrine ${{ matrix.doctrine }} | ||
phpunit-sqlite: | ||
name: PHPUnit (PHP ${{ matrix.php }} - Symfony ${{ matrix.symfony }} - Doctrine ${{ matrix.doctrine }} - SQLite - ${{ matrix.operating-system }}) | ||
runs-on: ${{ matrix.operating-system }} | ||
|
||
strategy: | ||
fail-fast: false | ||
matrix: | ||
operating-system: [ ubuntu-latest, windows-latest ] | ||
php: [ '8.1', '8.2', '8.3', '8.4' ] | ||
symfony: [ '5.4.*', '6.4.*', '7.2.*' ] | ||
doctrine: [ '^2.5', '^3.0' ] | ||
php: ['8.1', '8.2', '8.3', '8.4'] | ||
symfony: ['5.4.*', '6.4.*', '7.2.*'] | ||
doctrine: ['^2.5', '^3.0'] | ||
exclude: | ||
- { php: '8.1', symfony: '7.2.*' } | ||
- { php: '8.1', doctrine: '^3.0' } | ||
- { symfony: '5.4.*', doctrine: '^3.0' } | ||
|
||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: Setup PHP ${{ matrix.php }} | ||
uses: shivammathur/setup-php@v2 | ||
- uses: actions/checkout@v4 | ||
- uses: shivammathur/setup-php@v2 | ||
with: | ||
php-version: ${{ matrix.php }} | ||
tools: flex | ||
coverage: none | ||
- uses: ramsey/composer-install@v3 | ||
env: | ||
SYMFONY_REQUIRE: ${{ matrix.symfony }} | ||
with: | ||
composer-options: --with=doctrine/orm:${{ matrix.doctrine }} | ||
- run: vendor/bin/phpunit tests | ||
env: | ||
DATABASE_URL: 'sqlite:///:memory:' | ||
|
||
phpunit-mysql: | ||
name: PHPUnit (PHP ${{ matrix.php }} - Symfony ${{ matrix.symfony }} - Doctrine ${{ matrix.doctrine }} - MySQL) | ||
runs-on: ubuntu-latest | ||
|
||
- name: Install composer dependencies | ||
uses: ramsey/composer-install@v3 | ||
services: | ||
mysql: | ||
image: mysql:8.0 | ||
env: | ||
MYSQL_ALLOW_EMPTY_PASSWORD: yes | ||
MYSQL_DATABASE: test_db | ||
options: >- | ||
--health-cmd="mysqladmin ping" | ||
--health-interval=10s | ||
--health-timeout=5s | ||
--health-retries=3 | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
php: ['8.1', '8.2', '8.3', '8.4'] | ||
symfony: ['5.4.*', '6.4.*', '7.2.*'] | ||
doctrine: ['^2.5', '^3.0'] | ||
exclude: | ||
- { php: '8.1', symfony: '7.2.*' } | ||
- { php: '8.1', doctrine: '^3.0' } | ||
- { symfony: '5.4.*', doctrine: '^3.0' } | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: shivammathur/setup-php@v2 | ||
with: | ||
php-version: ${{ matrix.php }} | ||
tools: flex | ||
extensions: pdo_mysql | ||
coverage: none | ||
- uses: ramsey/composer-install@v3 | ||
env: | ||
SYMFONY_REQUIRE: ${{ matrix.symfony }} | ||
with: | ||
composer-options: --with=doctrine/orm:${{ matrix.doctrine }} | ||
- run: vendor/bin/phpunit tests | ||
env: | ||
DATABASE_URL: 'mysql://root:@127.0.0.1:3306/test_db' | ||
|
||
phpunit-postgres: | ||
name: PHPUnit (PHP ${{ matrix.php }} - Symfony ${{ matrix.symfony }} - Doctrine ${{ matrix.doctrine }} - PostgreSQL) | ||
runs-on: ubuntu-latest | ||
|
||
services: | ||
postgres: | ||
image: postgres:15 | ||
env: | ||
POSTGRES_PASSWORD: postgres | ||
POSTGRES_DB: test_db | ||
options: >- | ||
--health-cmd pg_isready | ||
--health-interval=10s | ||
--health-timeout=5s | ||
--health-retries=3 | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
php: ['8.1', '8.2', '8.3', '8.4'] | ||
symfony: ['5.4.*', '6.4.*', '7.2.*'] | ||
doctrine: ['^2.5', '^3.0'] | ||
exclude: | ||
- { php: '8.1', symfony: '7.2.*' } | ||
- { php: '8.1', doctrine: '^3.0' } | ||
- { symfony: '5.4.*', doctrine: '^3.0' } | ||
|
||
- name: Run test suite | ||
run: vendor/bin/phpunit tests | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: shivammathur/setup-php@v2 | ||
with: | ||
php-version: ${{ matrix.php }} | ||
tools: flex | ||
extensions: pdo_pgsql | ||
coverage: none | ||
- uses: ramsey/composer-install@v3 | ||
env: | ||
SYMFONY_REQUIRE: ${{ matrix.symfony }} | ||
with: | ||
composer-options: --with=doctrine/orm:${{ matrix.doctrine }} | ||
- run: vendor/bin/phpunit tests | ||
env: | ||
DATABASE_URL: 'postgresql://postgres:[email protected]:5432/test_db' | ||
|
||
ecs: | ||
name: Easy Coding Standard | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
|
||
doctrine: | ||
|
||
dbal: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Headsnet\DomainEventsBundle\Integration; | ||
|
||
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; | ||
use Doctrine\Bundle\DoctrineBundle\Registry; | ||
use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
use Doctrine\ORM\EntityManagerInterface; | ||
use Doctrine\ORM\Tools\SchemaTool; | ||
use Headsnet\DomainEventsBundle\Domain\Model\DomainEvent; | ||
use Headsnet\DomainEventsBundle\Domain\Model\EventStore; | ||
use Headsnet\DomainEventsBundle\Domain\Model\StoredEvent; | ||
use Headsnet\DomainEventsBundle\HeadsnetDomainEventsBundle; | ||
use Headsnet\DomainEventsBundle\Integration\Fixtures\TestEntity; | ||
use Headsnet\DomainEventsBundle\Integration\Fixtures\TestEvent; | ||
use Nyholm\BundleTest\TestKernel; | ||
use Ramsey\Uuid\Uuid; | ||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle; | ||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||
use Symfony\Component\HttpKernel\KernelInterface; | ||
|
||
/** | ||
* @group integration | ||
*/ | ||
class DomainEventPersistenceTest extends KernelTestCase | ||
{ | ||
private EntityManagerInterface $entityManager; | ||
private EventStore $eventStore; | ||
|
||
protected static function getKernelClass(): string | ||
{ | ||
return TestKernel::class; | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $options | ||
*/ | ||
protected static function createKernel(array $options = []): KernelInterface | ||
{ | ||
/** @var TestKernel $kernel */ | ||
$kernel = parent::createKernel($options); | ||
$kernel->addTestConfig(__DIR__ . '/config.yml'); | ||
$kernel->addTestBundle(FrameworkBundle::class); | ||
$kernel->addTestBundle(DoctrineBundle::class); | ||
$kernel->addTestBundle(HeadsnetDomainEventsBundle::class); | ||
$kernel->handleOptions($options); | ||
|
||
return $kernel; | ||
} | ||
|
||
protected function setUp(): void | ||
{ | ||
self::bootKernel(); | ||
|
||
$container = self::getContainer(); | ||
|
||
/** @var Registry $doctrine */ | ||
$doctrine = $container->get('doctrine'); | ||
/** @var EntityManagerInterface $entityManager */ | ||
$entityManager = $doctrine->getManager(); | ||
$this->entityManager = $entityManager; | ||
|
||
/** @var EventStore $eventStore */ | ||
$eventStore = $container->get(EventStore::class); | ||
$this->eventStore = $eventStore; | ||
|
||
$schemaTool = new SchemaTool($this->entityManager); | ||
$metadata = $this->entityManager->getMetadataFactory()->getAllMetadata(); | ||
$schemaTool->dropSchema($metadata); | ||
$schemaTool->createSchema($metadata); | ||
} | ||
|
||
public function testDomainEventIsPersistedWithCorrectData(): void | ||
{ | ||
$entity = new TestEntity(); | ||
$event = new TestEvent($entity->getId()); | ||
|
||
$entity->record($event); | ||
$this->entityManager->persist($entity); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
|
||
/** @var StoredEvent[] $storedEvents */ | ||
$storedEvents = $this->entityManager->getRepository(StoredEvent::class)->findAll(); | ||
self::assertCount(1, $storedEvents); | ||
self::assertEquals($event->getAggregateRootId(), $storedEvents[0]->getAggregateRoot()); | ||
/** @var \DateTimeImmutable $expectedDate */ | ||
$expectedDate = \DateTimeImmutable::createFromFormat(DomainEvent::MICROSECOND_DATE_FORMAT, $event->getOccurredOn()); | ||
/** @var \DateTimeImmutable $actualDate */ | ||
$actualDate = $storedEvents[0]->getOccurredOn(); | ||
$platform = $this->entityManager->getConnection()->getDatabasePlatform(); | ||
|
||
if ($this->isMicrosecondPlatform($platform)) { | ||
// MySQL and PostgreSQL support microseconds | ||
self::assertEquals( | ||
$expectedDate->format('Y-m-d\TH:i:s.u'), | ||
$actualDate->format('Y-m-d\TH:i:s.u'), | ||
'Dates should match with microsecond precision' | ||
); | ||
} else { | ||
// SQLite and others: compare only up to seconds | ||
self::assertEquals( | ||
$expectedDate->format('Y-m-d H:i:s'), | ||
$actualDate->format('Y-m-d H:i:s'), | ||
'Dates should match up to seconds precision' | ||
); | ||
} | ||
self::assertNull($storedEvents[0]->getPublishedOn()); | ||
} | ||
|
||
public function testReplaceableDomainEventReplacesUnpublishedEvent(): void | ||
{ | ||
$aggregateId = Uuid::uuid4()->toString(); | ||
$firstEvent = new TestEvent($aggregateId); | ||
$secondEvent = new TestEvent($aggregateId); | ||
|
||
// Store event | ||
$this->eventStore->append($firstEvent); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
|
||
$events = $this->entityManager->getRepository(StoredEvent::class)->findAll(); | ||
self::assertCount(1, $events); | ||
$firstStoredEventId = $events[0]->getEventId()->asString(); | ||
|
||
// Replace event | ||
$this->eventStore->replace($secondEvent); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
|
||
$events = $this->entityManager->getRepository(StoredEvent::class)->findAll(); | ||
self::assertCount(1, $events, 'Should still have only one event'); | ||
self::assertNotEquals($firstStoredEventId, $events[0]->getEventId()->asString(), 'Should have a different event ID'); | ||
self::assertEquals($aggregateId, $events[0]->getAggregateRoot()); | ||
} | ||
|
||
public function testPublishedDomainEventIsNotReplaced(): void | ||
{ | ||
$aggregateId = Uuid::uuid4()->toString(); | ||
$firstEvent = new TestEvent($aggregateId); | ||
|
||
$this->eventStore->append($firstEvent); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
$storedEvent = $this->entityManager->getRepository(StoredEvent::class)->findAll()[0]; | ||
$this->eventStore->publish($storedEvent); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
|
||
// Attempt to replace published event | ||
$secondEvent = new TestEvent($aggregateId); | ||
$this->eventStore->replace($secondEvent); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
|
||
$events = $this->entityManager->getRepository(StoredEvent::class)->findAll(); | ||
self::assertCount(2, $events, 'Should have two events'); | ||
self::assertEquals($aggregateId, $events[0]->getAggregateRoot()); | ||
self::assertEquals($aggregateId, $events[1]->getAggregateRoot()); | ||
self::assertNotNull($events[0]->getPublishedOn(), 'First event should be published'); | ||
self::assertNull($events[1]->getPublishedOn(), 'Second event should not be published'); | ||
} | ||
|
||
public function testUnpublishedEventsCanBeRetrieved(): void | ||
{ | ||
$aggregateId = Uuid::uuid4()->toString(); | ||
$event = new TestEvent($aggregateId); | ||
|
||
$this->eventStore->append($event); | ||
$this->entityManager->flush(); | ||
$this->entityManager->clear(); | ||
|
||
$unpublishedEvents = $this->eventStore->allUnpublished(); | ||
self::assertCount(1, $unpublishedEvents); | ||
self::assertEquals($aggregateId, $unpublishedEvents[0]->getAggregateRoot()); | ||
} | ||
|
||
private function isMicrosecondPlatform(AbstractPlatform $platform): bool | ||
{ | ||
$platformClass = get_class($platform); | ||
$microsecondPlatformClasses = [ | ||
'Doctrine\\DBAL\\Platforms\\MySQLPlatform', | ||
'Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform' | ||
]; | ||
|
||
return in_array($platformClass, $microsecondPlatformClasses); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
parent::tearDown(); | ||
$this->entityManager->close(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Headsnet\DomainEventsBundle\Integration\Fixtures; | ||
|
||
use Doctrine\ORM\Mapping as ORM; | ||
use Headsnet\DomainEventsBundle\Domain\Model\ContainsEvents; | ||
use Headsnet\DomainEventsBundle\Domain\Model\RecordsEvents; | ||
use Headsnet\DomainEventsBundle\Domain\Model\Traits\EventRecorderTrait; | ||
use Ramsey\Uuid\Uuid; | ||
|
||
#[ORM\Entity] | ||
class TestEntity implements RecordsEvents, ContainsEvents | ||
{ | ||
use EventRecorderTrait; | ||
|
||
#[ORM\Id] | ||
#[ORM\Column(type: 'string', length: 36)] | ||
private string $id; | ||
|
||
public function __construct() | ||
{ | ||
$this->id = Uuid::uuid4()->toString(); | ||
} | ||
|
||
public function getId(): string | ||
{ | ||
return $this->id; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Headsnet\DomainEventsBundle\Integration\Fixtures; | ||
|
||
use Headsnet\DomainEventsBundle\Domain\Model\DomainEvent; | ||
use Headsnet\DomainEventsBundle\Domain\Model\Traits\DomainEventTrait; | ||
|
||
class TestEvent implements DomainEvent | ||
{ | ||
use DomainEventTrait; | ||
|
||
public function __construct(string $aggregateRootId) | ||
{ | ||
$this->aggregateRootId = $aggregateRootId; | ||
$this->occurredOn = (new \DateTimeImmutable())->format(DomainEvent::MICROSECOND_DATE_FORMAT); | ||
} | ||
} |
Oops, something went wrong.