Skip to content

Commit

Permalink
feat: introduce EveryTestHasSameNamespaceAsCoveredClass check (#38)
Browse files Browse the repository at this point in the history
Replaces EveryTestHasSameNamespaceAsTestedClass check
  • Loading branch information
simPod authored Apr 21, 2022
1 parent d72fcd8 commit 58efa39
Show file tree
Hide file tree
Showing 16 changed files with 357 additions and 1 deletion.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ self::assertSame('Hello world!', $myEntity->salute());

Test Checks are used to assert that tests comply with your suite's standards (are final, extend correct TestCaseBase etc.)

To run them, eg. create a test case like in the following example:
To run them, e.g. create a test case like in the following example:

```php
<?php
Expand Down Expand Up @@ -140,6 +140,57 @@ yield 'Every test has group' => [
];
```

### Every test has same namespace as covered class

Asserts that all test share same namespace with class they're testing.
Consider src namespace `Ns` and test namespace `Ns/Tests` then for test `Ns/Tests/UnitTest` must exist class `Ns/Unit`.

You can use `@covers` or `@coversDefaultClass` annotations to link test with tested class.
Use `@coversNothing` annotation to skip this check.

Don't forget to enable `"forceCoversAnnotation="true"` in phpunit config file.

```php
namespace Ns;

final class Unit {}
```

:x:
```php
namespace Ns\Tests;

final class NonexistentUnitTest extends TestCase {}
```

```php
namespace Ns\Tests\Sub;

final class UnitTest extends TestCase {}
```

:heavy_check_mark:
```php
namespace Ns\Tests;

final class UnitTest extends TestCase {}
```

```php
namespace Ns\Tests\Sub;

/** @covers \Ns\Unit */
final class UnitTest extends TestCase {}
```

Configured in test provider as

```php
yield 'Every test has same namespace as tested class' => [
new EveryTestHasSameNamespaceAsCoveredClass($testFiles),
];
```

### Every test has same namespace as tested class

Asserts that all test share same namespace with class they're testing.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"autoload-dev": {
"psr-4": {
"Cdn77\\TestUtils\\Tests\\": "tests/",
"Cdn77\\TestUtils\\Tests\\Tests\\TestCheck\\Fixtures\\EveryTestHasSameNamespaceAsCoveredClass\\": "tests/TestCheck/Fixtures/EveryTestHasSameNamespaceAsCoveredClass/tests",
"Cdn77\\TestUtils\\Tests\\Tests\\TestCheck\\Fixtures\\EveryTestHasSameNamespaceAsTestedClass\\": "tests/TestCheck/Fixtures/EveryTestHasSameNamespaceAsTestedClass/tests"
}
},
Expand Down
113 changes: 113 additions & 0 deletions src/TestCheck/EveryTestHasSameNamespaceAsCoveredClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\TestCheck;

use Cdn77\EntityFqnExtractor\ClassExtractor;
use PHPUnit\Framework\TestCase;
use ReflectionClass;

use function class_exists;
use function count;
use function Safe\preg_match;
use function Safe\preg_match_all;
use function Safe\sprintf;
use function Safe\substr;
use function strlen;
use function strpos;
use function substr_replace;
use function trait_exists;

final class EveryTestHasSameNamespaceAsCoveredClass implements TestCheck
{
private const PATTERN_COVERS = '~\* @covers(DefaultClass)? +(?<coveredClass>.+?)(?:\n| \*/)~';
private const PATTERN_COVERS_NOTHING = '~\* @coversNothing~';

private string $testsNamespaceSuffix;

/** @param iterable<string> $filePathNames */
public function __construct(private iterable $filePathNames, string $testsNamespaceSuffix = 'Tests')
{
$this->testsNamespaceSuffix = '\\' . $testsNamespaceSuffix . '\\';
}

public function run(TestCase $testCaseContext): void
{
$testCaseContext::assertTrue(true);

foreach ($this->filePathNames as $file) {
$classReflection = new ReflectionClass(ClassExtractor::get($file));

$docComment = $classReflection->getDocComment();
if ($docComment === false) {
$docComment = '';
}

$matchesCovers = preg_match_all(self::PATTERN_COVERS, $docComment, $coversMatches) > 0;
$matchesCoversNothing = preg_match(self::PATTERN_COVERS_NOTHING, $docComment) === 1;

if ($matchesCovers && $matchesCoversNothing) {
$testCaseContext::fail(sprintf(
'Test file "%s" contains both @covers and @coversNothing annotations.',
$file
));
}

if ($matchesCoversNothing) {
continue;
}

$className = $classReflection->getName();
$classNameWithoutSuffix = substr($className, 0, -4);
$pos = strpos($classNameWithoutSuffix, $this->testsNamespaceSuffix);
if ($pos === false) {
$coveredClassName = $classNameWithoutSuffix;
} else {
$coveredClassName = substr_replace(
$classNameWithoutSuffix,
'\\',
$pos,
strlen($this->testsNamespaceSuffix)
);
}

if (class_exists($coveredClassName) || trait_exists($coveredClassName)) {
continue;
}

if (class_exists($classNameWithoutSuffix)) {
continue;
}

if ($coversMatches[0] === []) {
$testCaseContext::fail(
sprintf(
'Test "%s" is in the wrong namespace, ' .
'has name different from tested class or is missing @covers annotation',
$classReflection->getName()
)
);
}

/** @psalm-var list<class-string> $coveredClass */
$coveredClasses = $coversMatches['coveredClass'];
if (count($coveredClasses) > 1) {
continue;
}

$coveredClass = $coveredClasses[0];
if (class_exists($coveredClass)) {
continue;
}

$testCaseContext::fail(
sprintf(
'Test %s is pointing to an non-existing class "%s"',
$classReflection->getName(),
$coveredClass
)
);
}
}
}
1 change: 1 addition & 0 deletions src/TestCheck/EveryTestHasSameNamespaceAsTestedClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use function substr_replace;
use function trait_exists;

/** @deprecated Use {@see EveryTestHasSameNamespaceAsCoveredClass} */
final class EveryTestHasSameNamespaceAsTestedClass implements TestCheck
{
private const PATTERN = '~\* @testedClass (?<targetClass>.+?)(?:\n| \*/)~';
Expand Down
77 changes: 77 additions & 0 deletions tests/TestCheck/EveryTestHasSameNamespaceAsCoveredClassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\TestCheck;

use Cdn77\TestUtils\TestCheck\EveryTestHasSameNamespaceAsCoveredClass;
use Cdn77\TestUtils\Tests\BaseTestCase;
use Generator;
use PHPUnit\Framework\AssertionFailedError;

final class EveryTestHasSameNamespaceAsCoveredClassTest extends BaseTestCase
{
/** @dataProvider providerSuccess */
public function testSuccess(string $filePath): void
{
$check = new EveryTestHasSameNamespaceAsCoveredClass(
[__DIR__ . '/Fixtures/EveryTestHasSameNamespaceAsCoveredClass/tests/' . $filePath],
'Tests'
);
$check->run($this);
}

/** @return Generator<array-key, list<string>> */
public function providerSuccess(): Generator
{
$files = [
'IgnoreMultipleCoversTest.php',
'SameNamespaceTest.php',
'SameNamespaceAsLinkedCoveredClassTest.php',
'CoveredClassWithSomeWhitespaceTest.php',
'CoversNothingTest.php',
'CoversDefaultClassTest.php',
];

foreach ($files as $file) {
yield $file => [$file];
}
}

/** @dataProvider providerFail */
public function testFail(string $filePath, string $error): void
{
$this->expectException(AssertionFailedError::class);
$this->expectExceptionMessage($error);

$check = new EveryTestHasSameNamespaceAsCoveredClass(
[__DIR__ . '/Fixtures/EveryTestHasSameNamespaceAsCoveredClass/tests/' . $filePath],
'Tests'
);
$check->run($this);
}

/** @return Generator<array-key, list<string>> */
public function providerFail(): Generator
{
yield [
'CoversNonexistentClassTest.php',
'is pointing to an non-existing class',
];

yield [
'CoversAndCoversNothingTest.php',
'contains both @covers and @coversNothing annotations',
];

yield [
'SubNamespace/SameNamespaceTest.php',
'is in the wrong namespace',
];

yield [
'SameNamespaceWrongNameTest.php',
'is in the wrong namespace',
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

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

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

/** @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass\SameNamespace */
final class CoveredClassWithSomeWhitespaceTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

/**
* @coversNothing
* @covers \stdClass
*/
final class CoversAndCoversNothingTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

// phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
/** @coversDefaultClass Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass\SameNamespace */
final class CoversDefaultClassTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

/** @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsTestedClass\Noexists */
final class CoversNonexistentClassTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

/** @coversNothing */
final class CoversNothingTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

/**
* @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsTestedClass\A
* @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsTestedClass\B
*/
final class IgnoreMultipleCoversTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

/** @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass\SameNamespace */
final class SameNamespaceAsLinkedCoveredClassTest
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

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

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;

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

declare(strict_types=1);

namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass\SubNamespace;

final class SameNamespaceTest
{
}

0 comments on commit 58efa39

Please sign in to comment.