From 95821e4c90a2fddb768e92f9342acfcdff092794 Mon Sep 17 00:00:00 2001 From: Herberto Graca Date: Wed, 26 Jul 2023 23:04:59 +0200 Subject: [PATCH] Create a HaveCorrespondingUnit expression This will allow us to ensure that certain classes always have a test, or that every test has a matching class and their namespaces are correct. --- README.md | 19 +++++ .../ForClasses/HaveCorrespondingUnit.php | 57 ++++++++++++++ .../ForClasses/DummyClasses/Cat.php | 9 +++ .../ForClasses/DummyClasses/CatTestCase.php | 9 +++ .../ForClasses/DummyClasses/Dog.php | 9 +++ .../ForClasses/HaveCorrespondingUnitTest.php | 77 +++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 src/Expression/ForClasses/HaveCorrespondingUnit.php create mode 100644 tests/Unit/Expressions/ForClasses/DummyClasses/Cat.php create mode 100644 tests/Unit/Expressions/ForClasses/DummyClasses/CatTestCase.php create mode 100644 tests/Unit/Expressions/ForClasses/DummyClasses/Dog.php create mode 100644 tests/Unit/Expressions/ForClasses/HaveCorrespondingUnitTest.php diff --git a/README.md b/README.md index c9659c4e..ea01d906 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,25 @@ For example: `phparkitect debug:expression ResideInOneOfTheseNamespaces App` Currently, you can check if a class: +### Has a corresponding code unit in another namespace + +This will allow us to ensure that certain classes always have a test, +or that every test has a matching class and their namespaces are correct. + +```php +$rules = Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('App\Core\Component\**\Command\*')) + ->should(new HaveCorrespondingUnit( + // This will assert that class `App\Core\Component\MyComponent\Command\MyCommand` + // has a test class in `Tests\App\Core\Component\MyComponent\Command\MyCommandTest` + function ($fqcn) { + return 'Tests\'.$fqcn.'Test'; + } + ) + ) + ->because('we want all our command handlers to have a test'); +``` + ### Depends on a namespace ```php diff --git a/src/Expression/ForClasses/HaveCorrespondingUnit.php b/src/Expression/ForClasses/HaveCorrespondingUnit.php new file mode 100644 index 00000000..6ac84dfb --- /dev/null +++ b/src/Expression/ForClasses/HaveCorrespondingUnit.php @@ -0,0 +1,57 @@ +inferFqnFunction = $inferFqnFunction; + } + + public function describe(ClassDescription $theClass, string $because): Description + { + $correspondingFqn = $this->inferCorrespondingFqn($theClass); + + return new Description("should have a matching unit named: '$correspondingFqn'", $because); + } + + public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void + { + $correspondingFqn = $this->inferCorrespondingFqn($theClass); + + if ( + !trait_exists($correspondingFqn) + && !class_exists($correspondingFqn) + && !interface_exists($correspondingFqn) + ) { + $violations->add( + new Violation( + $theClass->getFQCN(), + $this->describe($theClass, $because)->toString() + ) + ); + } + } + + /** + * @return class-string + */ + public function inferCorrespondingFqn(ClassDescription $theClass): string + { + $inferFqn = $this->inferFqnFunction; + + return $inferFqn($theClass->getFQCN()); + } +} diff --git a/tests/Unit/Expressions/ForClasses/DummyClasses/Cat.php b/tests/Unit/Expressions/ForClasses/DummyClasses/Cat.php new file mode 100644 index 00000000..d3145233 --- /dev/null +++ b/tests/Unit/Expressions/ForClasses/DummyClasses/Cat.php @@ -0,0 +1,9 @@ +evaluate($classDescription, $violations, $because); + + self::assertEquals(0, $violations->count()); + } + + public function test_it_should_return_violation_error(): void + { + $class = Dog::class; + $classDescription = new ClassDescription( + FullyQualifiedClassName::fromString($class), + [], + [], + null, + false, + false, + false, + false, + false + ); + $constraint = new HaveCorrespondingUnit( + function ($fqn) { + return $fqn.'TestCase'; + } + ); + + $because = 'we want all our command handlers to have a test'; + $violations = new Violations(); + $constraint->evaluate($classDescription, $violations, $because); + + self::assertNotEquals(0, $violations->count()); + + $violationError = $constraint->describe($classDescription, $because)->toString(); + $this->assertEquals( + 'should have a matching unit named: ' + ."'Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\DogTestCase' because $because", + $violationError + ); + } +}