diff --git a/tests/Architecture/ApplicationTest.php b/tests/Architecture/ApplicationTest.php index 83382a1..70621b2 100644 --- a/tests/Architecture/ApplicationTest.php +++ b/tests/Architecture/ApplicationTest.php @@ -2,30 +2,54 @@ declare(strict_types=1); -describe('application coding rules', function (): void { - arch('no Domain Entities are used in Controllers') - ->expect('Application\\Http\\Controllers') - ->not->toUse('Domain\\Models\\Entities'); +use Shared\DDD\UseCases\Input; +use Shared\DDD\UseCases\Output; +use Shared\DDD\UseCases\UseCase; - arch('no Domain Entities are used in Policies') - ->expect('Application\\Policies') - ->not->toUse('Domain\\Models\\Entities'); - - arch('app Repositories implement Application\Contracts\Repository') - ->expect('Application\\Repositories') - ->toImplement('Application\\Contracts\\Repository'); - - arch('app Directories implement Application\Contracts\Directory') - ->expect('Application\\Directories') - ->toImplement('Application\\Contracts\\Directory'); - - arch('app DTOs are final readonly classes') - ->expect('Application\\Dto') +describe('application layer architecture rules', function (): void { + arch('use cases implement UseCase and are final') + ->expect('Application\\UseCases') ->classes() - ->toBeFinal() + ->implementing(UseCase::class) + ->toBeFinal(); + + arch('use cases implement UseCase and are readonly') + ->expect('Application\\UseCases') + ->classes() + ->implementing(UseCase::class) ->toBeReadonly(); - arch('no debug calls in production code') - ->expect(['dd', 'dump', 'die', 'var_dump', 'sleep']) - ->not->toBeUsed(); + arch('use case inputs implement Input interface and are final') + ->expect('Application\\UseCases') + ->classes() + ->implementing(Input::class) + ->toBeFinal(); + + arch('use case inputs implement Input interface and are readonly') + ->expect('Application\\UseCases') + ->classes() + ->implementing(Input::class) + ->toBeReadonly(); + + arch('use case outputs implement Output interface and are final') + ->expect('Application\\UseCases') + ->classes() + ->implementing(Output::class) + ->toBeFinal(); + + arch('use case outputs implement Output interface and are readonly') + ->expect('Application\\UseCases') + ->classes() + ->implementing(Output::class) + ->toBeReadonly(); + + arch('application exceptions extend Exception and are final') + ->expect('Application') + ->classes() + ->extending(\Exception::class) + ->toBeFinal(); + + arch('application layer does not use Infrastructure or framework internals') + ->expect('Application') + ->not->toUse(['Infrastructure', 'Illuminate', 'Symfony']); }); diff --git a/tests/Architecture/CodingStandardsTest.php b/tests/Architecture/CodingStandardsTest.php index 07ce197..1edbe6e 100644 --- a/tests/Architecture/CodingStandardsTest.php +++ b/tests/Architecture/CodingStandardsTest.php @@ -6,47 +6,19 @@ describe('coding standards', function (): void { arch()->preset()->php(); arch()->preset()->security(); - arch('it has no shell execution functions are used') - ->expect(['exec', 'shell_exec', 'system', 'passthru', 'proc_open']) - ->not->toBeUsed(); + arch('all source files use strict types') + ->expect(['Application', 'Domain', 'Shared']) + ->toUseStrictTypes(); - arch('it has no eval usage') - ->expect(['eval']) - ->not->toBeUsed(); - - arch('it has no dangerous file functions') - ->expect(['file_get_contents', 'file_put_contents', 'fopen']) - ->not->toBeUsed(); - - arch('it has no globals are used') - ->expect(['$GLOBALS', 'global']) - ->not->toBeUsed(); - - arch('it has no singletons') - ->expect(['getInstance', 'instance']) - ->not->toBeUsed(); - - arch('it has no insecure hashing') - ->expect(['md5', 'sha1']) - ->not->toBeUsed(); - - arch('it has no sleep function are used') + arch('no sleep functions in production code') ->expect(['sleep', 'usleep']) ->not->toBeUsed(); - arch('it has no debug globals are used') - ->expect(['var_dump', 'die', 'print_r']) + arch('no singletons in production code') + ->expect(['getInstance', 'instance']) ->not->toBeUsed(); - arch('all app source files use strict types') - ->expect('Application') - ->toUseStrictTypes(); - - arch('all domain source files use strict types') - ->expect('Domain') - ->toUseStrictTypes(); - - arch('all shared source files use strict types') - ->expect('Shared') - ->toUseStrictTypes(); + arch('no direct filesystem writes in domain or application') + ->expect(['Application', 'Domain']) + ->not->toUse(['file_put_contents', 'fopen']); }); diff --git a/tests/Architecture/DomainTest.php b/tests/Architecture/DomainTest.php index 3172a19..c7fc787 100644 --- a/tests/Architecture/DomainTest.php +++ b/tests/Architecture/DomainTest.php @@ -5,43 +5,39 @@ declare(strict_types=1); use Shared\DDD\Models\Entity; use Shared\DDD\Ports\Repository; -describe('domain coding rules', function (): void { - arch('entities implements Entity') +describe('domain layer architecture rules', function (): void { + arch('entities implement Entity and are final readonly') ->expect('Domain\\Models\\Entities') - ->toImplement(Entity::class); - - arch('entities are immutable (disabled due to mock-final-classes conflict)') - ->expect('Domain\\Models\\Entities') - ->toBeReadonly() - ->toBeFinal(); - - arch('valueObjects are immutable (disabled due to mock-final-classes conflict)') - ->expect('Domain\\Models\\ValueObjects') - ->toBeReadonly() - ->toBeFinal(); - - arch('ports contains only interfaces') - ->expect('Domain\\Ports') ->classes() - ->toBeInterface(); + ->toImplement(Entity::class) + ->toBeFinal() + ->toBeReadonly(); - arch('port Repositories extend Shared\DDD\Repository') + arch('value objects classes are readonly') + ->expect('Domain\\Models\\ValueObjects') + ->classes() + ->toBeReadonly(); + + arch('value objects classes are final') + ->expect('Domain\\Models\\ValueObjects') + ->classes() + ->toBeFinal(); + + arch('domain exceptions extend DomainException and are final') + ->expect('Domain\\Exceptions') + ->classes() + ->toExtend(\DomainException::class) + ->toBeFinal(); + + arch('ports contain only interfaces') + ->expect('Domain\\Ports') + ->toBeInterfaces(); + + arch('repository ports extend Shared Repository') ->expect('Domain\\Ports\\Repositories') ->toExtend(Repository::class); - arch('no Application classes are used in Domain') + arch('domain does not depend on external layers or frameworks') ->expect('Domain') - ->not->toUse('Application'); - - arch('no Infrastructure classes are used in Domain') - ->expect('Domain') - ->not->toUse('Infrastructure'); - - arch('no Illuminate classes are used in Domain') - ->expect('Domain') - ->not->toUse('Illuminate'); - - arch('no Symfony components are used in Domain') - ->expect('Domain') - ->not->toUse('Symfony'); + ->not->toUse(['Application', 'Infrastructure', 'Illuminate', 'Symfony']); }); diff --git a/tests/Architecture/NamingConventionsTest.php b/tests/Architecture/NamingConventionsTest.php index 4c3e0fa..e6225fa 100644 --- a/tests/Architecture/NamingConventionsTest.php +++ b/tests/Architecture/NamingConventionsTest.php @@ -2,62 +2,23 @@ declare(strict_types=1); -describe('naming conventions in application', function (): void { - arch('casts have Cast suffix') - ->expect('Application\\Cast') - ->toHaveSuffix('Cast'); +use Shared\DDD\UseCases\Input; - arch('adapters repositories have Repository suffix') - ->expect('Application\\Adapters\\Repositories') - ->toHaveSuffix('Repository'); - - arch('exceptions have Exception suffix') - ->expect('Application\\Exceptions') +describe('naming conventions', function (): void { + arch('all exceptions have Exception suffix') + ->expect(['Application', 'Domain', 'Shared']) + ->classes() + ->extending(\Throwable::class) ->toHaveSuffix('Exception'); - arch('controllers have Controller suffix') - ->expect('Application\\Http\\Controllers') - ->toHaveSuffix('Controller'); - - arch('requests have Request suffix') - ->expect('Application\\Http\\Requests') - ->toHaveSuffix('Request'); - - arch('resources have Resource suffix') - ->expect('Application\\Http\\Resources') - ->toHaveSuffix('Resource'); - - arch('policies have Policy suffix') - ->expect('Application\\Policies') - ->toHaveSuffix('Policy'); - - arch('providers have Provider suffix') - ->expect('Application\\Providers') - ->toHaveSuffix('Provider'); - - arch('services have Service suffix') - ->expect('Application\\Services') - ->toHaveSuffix('Service'); -}); - -describe('naming conventions in domain', function (): void { - arch('dtos have Dto suffix') - ->expect('Domain\\Dto') - ->toHaveSuffix('Dto'); - - arch('exceptions have Exception suffix') - ->expect('Domain\\Exceptions') - ->toHaveSuffix('Exception'); - - arch('policies have Policy suffix') - ->expect('Domain\\Policies') - ->toHaveSuffix('Policy'); - - arch('port repositories have Repository suffix') + arch('port repository interfaces have Repository suffix') ->expect('Domain\\Ports\\Repositories') + ->interfaces() ->toHaveSuffix('Repository'); - arch('domain services have Service suffix') - ->expect('Domain\\Services') - ->toHaveSuffix('Service'); + arch('use case inputs are named Input') + ->expect('Application\\UseCases') + ->classes() + ->implementing(Input::class) + ->toHaveSuffix('Input'); }); diff --git a/tests/Architecture/SharedTest.php b/tests/Architecture/SharedTest.php index 9108b5c..958e0f1 100644 --- a/tests/Architecture/SharedTest.php +++ b/tests/Architecture/SharedTest.php @@ -2,46 +2,31 @@ declare(strict_types=1); -use Shared\DDD\Models\Entity; -use Shared\DDD\Ports\Repository; - -describe('shared coding rules', function (): void { - arch('entities implements Entity') - ->expect('Shared\\Domain\\Models\\Entities') - ->toImplement(Entity::class); - - arch('entities are immutable (disabled due to mock-final-classes conflict)') - ->expect('Shared\\Entities') - ->toBeReadonly() - ->toBeFinal(); - - arch('valueObjects are immutable (disabled due to mock-final-classes conflict)') - ->expect('Shared\\Models\\ValueObjects') - ->toBeReadonly() - ->toBeFinal(); - - arch('ports contains only interfaces') - ->expect('Shared\\Ports') +describe('shared layer architecture rules', function (): void { + arch('shared value objects classes are final') + ->expect('Shared\\ValueObjects') ->classes() - ->toBeInterface(); + ->toBeFinal(); - arch('port Repositories extend Shared\DDD\Repository') - ->expect('Shared\\Ports\\Repositories') - ->toExtend(Repository::class); + arch('shared value objects classes are readonly') + ->expect('Shared\\ValueObjects') + ->classes() + ->toBeReadonly(); - arch('no Application classes are used in Shared') + arch('shared ports contain only interfaces') + ->expect('Shared\\DDD\\Ports') + ->toBeInterfaces(); + + arch('shared usecase contracts are interfaces') + ->expect('Shared\\DDD\\UseCases') + ->toBeInterfaces(); + + arch('shared exceptions extend Exception') + ->expect('Shared\\DDD\\Exceptions') + ->classes() + ->toExtend(\Exception::class); + + arch('shared layer is completely isolated from other application layers') ->expect('Shared') - ->not->toUse('Application'); - - arch('no Infrastructure classes are used in Shared') - ->expect('Shared') - ->not->toUse('Infrastructure'); - - arch('no Illuminate classes are used in Shared') - ->expect('Shared') - ->not->toUse('Illuminate'); - - arch('no Symfony components are used in Shared') - ->expect('Shared') - ->not->toUse('Symfony'); + ->not->toUse(['Application', 'Domain', 'Infrastructure', 'Illuminate', 'Symfony']); }); diff --git a/tests/Pest.php b/tests/Pest.php index 0983797..1b4fd15 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -5,23 +5,11 @@ declare(strict_types=1); use DG\BypassFinals; use Tests\TestCase; -// Prevents mock-final-classes/bypass-finals from removing final/readonly. -if (class_exists(BypassFinals::class)) { - BypassFinals::denyPaths([ - dirname(__DIR__) . '/src/*', - ]); -} - -/* - *-------------------------------------------------------------------------- - * Test Case - *-------------------------------------------------------------------------- - * - * The closure you provide to your test functions is always bound to a specific PHPUnit test - * case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may - * need to change it using the "pest()" function to bind different classes or traits. - * - */ +BypassFinals::enable(bypassReadOnly: false, bypassFinal: true); +BypassFinals::denyPaths([ + '*/src/*', + '*/tests/*', +]); pest()->extend(TestCase::class) ->in('Architecture', 'Unit'); @@ -30,16 +18,3 @@ pest()->extend(TestCase::class) pest()->group('architecture')->in('Architecture'); pest()->group('unit')->in('Unit'); - -/* - *-------------------------------------------------------------------------- - * Expectations - *-------------------------------------------------------------------------- - * - * When you're writing tests, you often need to check that values meet certain conditions. The - * "expect()" function gives you access to a set of "expectations" methods that you can use - * to assert different things. Of course, you may extend the Expectation API at any time. - * - */ - -expect()->extend('toBeOne', fn () => $this->toBe(1));