Skip to content

Commit

Permalink
Create a HaveCorrespondingUnit expression
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Herberto Graca authored and hgraca committed Sep 5, 2023
1 parent 976c200 commit 95821e4
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions src/Expression/ForClasses/HaveCorrespondingUnit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Arkitect\Expression\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Expression\Description;
use Arkitect\Expression\Expression;
use Arkitect\Rules\Violation;
use Arkitect\Rules\Violations;

final class HaveCorrespondingUnit implements Expression
{
/** @var \Closure */
private $inferFqnFunction;

public function __construct(\Closure $inferFqnFunction)
{
$this->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());
}
}
9 changes: 9 additions & 0 deletions tests/Unit/Expressions/ForClasses/DummyClasses/Cat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

final class Cat
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

final class CatTestCase
{
}
9 changes: 9 additions & 0 deletions tests/Unit/Expressions/ForClasses/DummyClasses/Dog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

final class Dog
{
}
77 changes: 77 additions & 0 deletions tests/Unit/Expressions/ForClasses/HaveCorrespondingUnitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Analyzer\FullyQualifiedClassName;
use Arkitect\Expression\ForClasses\HaveCorrespondingUnit;
use Arkitect\Rules\Violations;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\Cat;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\Dog;
use PHPUnit\Framework\TestCase;

class HaveCorrespondingUnitTest extends TestCase
{
public function test_it_should_pass_the_validation(): void
{
$class = Cat::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::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
);
}
}

0 comments on commit 95821e4

Please sign in to comment.