Skip to content

Commit 58efa39

Browse files
authored
feat: introduce EveryTestHasSameNamespaceAsCoveredClass check (#38)
Replaces EveryTestHasSameNamespaceAsTestedClass check
1 parent d72fcd8 commit 58efa39

16 files changed

+357
-1
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ self::assertSame('Hello world!', $myEntity->salute());
7979

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

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

8484
```php
8585
<?php
@@ -140,6 +140,57 @@ yield 'Every test has group' => [
140140
];
141141
```
142142

143+
### Every test has same namespace as covered class
144+
145+
Asserts that all test share same namespace with class they're testing.
146+
Consider src namespace `Ns` and test namespace `Ns/Tests` then for test `Ns/Tests/UnitTest` must exist class `Ns/Unit`.
147+
148+
You can use `@covers` or `@coversDefaultClass` annotations to link test with tested class.
149+
Use `@coversNothing` annotation to skip this check.
150+
151+
Don't forget to enable `"forceCoversAnnotation="true"` in phpunit config file.
152+
153+
```php
154+
namespace Ns;
155+
156+
final class Unit {}
157+
```
158+
159+
:x:
160+
```php
161+
namespace Ns\Tests;
162+
163+
final class NonexistentUnitTest extends TestCase {}
164+
```
165+
166+
```php
167+
namespace Ns\Tests\Sub;
168+
169+
final class UnitTest extends TestCase {}
170+
```
171+
172+
:heavy_check_mark:
173+
```php
174+
namespace Ns\Tests;
175+
176+
final class UnitTest extends TestCase {}
177+
```
178+
179+
```php
180+
namespace Ns\Tests\Sub;
181+
182+
/** @covers \Ns\Unit */
183+
final class UnitTest extends TestCase {}
184+
```
185+
186+
Configured in test provider as
187+
188+
```php
189+
yield 'Every test has same namespace as tested class' => [
190+
new EveryTestHasSameNamespaceAsCoveredClass($testFiles),
191+
];
192+
```
193+
143194
### Every test has same namespace as tested class
144195

145196
Asserts that all test share same namespace with class they're testing.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"autoload-dev": {
1212
"psr-4": {
1313
"Cdn77\\TestUtils\\Tests\\": "tests/",
14+
"Cdn77\\TestUtils\\Tests\\Tests\\TestCheck\\Fixtures\\EveryTestHasSameNamespaceAsCoveredClass\\": "tests/TestCheck/Fixtures/EveryTestHasSameNamespaceAsCoveredClass/tests",
1415
"Cdn77\\TestUtils\\Tests\\Tests\\TestCheck\\Fixtures\\EveryTestHasSameNamespaceAsTestedClass\\": "tests/TestCheck/Fixtures/EveryTestHasSameNamespaceAsTestedClass/tests"
1516
}
1617
},
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\TestCheck;
6+
7+
use Cdn77\EntityFqnExtractor\ClassExtractor;
8+
use PHPUnit\Framework\TestCase;
9+
use ReflectionClass;
10+
11+
use function class_exists;
12+
use function count;
13+
use function Safe\preg_match;
14+
use function Safe\preg_match_all;
15+
use function Safe\sprintf;
16+
use function Safe\substr;
17+
use function strlen;
18+
use function strpos;
19+
use function substr_replace;
20+
use function trait_exists;
21+
22+
final class EveryTestHasSameNamespaceAsCoveredClass implements TestCheck
23+
{
24+
private const PATTERN_COVERS = '~\* @covers(DefaultClass)? +(?<coveredClass>.+?)(?:\n| \*/)~';
25+
private const PATTERN_COVERS_NOTHING = '~\* @coversNothing~';
26+
27+
private string $testsNamespaceSuffix;
28+
29+
/** @param iterable<string> $filePathNames */
30+
public function __construct(private iterable $filePathNames, string $testsNamespaceSuffix = 'Tests')
31+
{
32+
$this->testsNamespaceSuffix = '\\' . $testsNamespaceSuffix . '\\';
33+
}
34+
35+
public function run(TestCase $testCaseContext): void
36+
{
37+
$testCaseContext::assertTrue(true);
38+
39+
foreach ($this->filePathNames as $file) {
40+
$classReflection = new ReflectionClass(ClassExtractor::get($file));
41+
42+
$docComment = $classReflection->getDocComment();
43+
if ($docComment === false) {
44+
$docComment = '';
45+
}
46+
47+
$matchesCovers = preg_match_all(self::PATTERN_COVERS, $docComment, $coversMatches) > 0;
48+
$matchesCoversNothing = preg_match(self::PATTERN_COVERS_NOTHING, $docComment) === 1;
49+
50+
if ($matchesCovers && $matchesCoversNothing) {
51+
$testCaseContext::fail(sprintf(
52+
'Test file "%s" contains both @covers and @coversNothing annotations.',
53+
$file
54+
));
55+
}
56+
57+
if ($matchesCoversNothing) {
58+
continue;
59+
}
60+
61+
$className = $classReflection->getName();
62+
$classNameWithoutSuffix = substr($className, 0, -4);
63+
$pos = strpos($classNameWithoutSuffix, $this->testsNamespaceSuffix);
64+
if ($pos === false) {
65+
$coveredClassName = $classNameWithoutSuffix;
66+
} else {
67+
$coveredClassName = substr_replace(
68+
$classNameWithoutSuffix,
69+
'\\',
70+
$pos,
71+
strlen($this->testsNamespaceSuffix)
72+
);
73+
}
74+
75+
if (class_exists($coveredClassName) || trait_exists($coveredClassName)) {
76+
continue;
77+
}
78+
79+
if (class_exists($classNameWithoutSuffix)) {
80+
continue;
81+
}
82+
83+
if ($coversMatches[0] === []) {
84+
$testCaseContext::fail(
85+
sprintf(
86+
'Test "%s" is in the wrong namespace, ' .
87+
'has name different from tested class or is missing @covers annotation',
88+
$classReflection->getName()
89+
)
90+
);
91+
}
92+
93+
/** @psalm-var list<class-string> $coveredClass */
94+
$coveredClasses = $coversMatches['coveredClass'];
95+
if (count($coveredClasses) > 1) {
96+
continue;
97+
}
98+
99+
$coveredClass = $coveredClasses[0];
100+
if (class_exists($coveredClass)) {
101+
continue;
102+
}
103+
104+
$testCaseContext::fail(
105+
sprintf(
106+
'Test %s is pointing to an non-existing class "%s"',
107+
$classReflection->getName(),
108+
$coveredClass
109+
)
110+
);
111+
}
112+
}
113+
}

src/TestCheck/EveryTestHasSameNamespaceAsTestedClass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use function substr_replace;
1818
use function trait_exists;
1919

20+
/** @deprecated Use {@see EveryTestHasSameNamespaceAsCoveredClass} */
2021
final class EveryTestHasSameNamespaceAsTestedClass implements TestCheck
2122
{
2223
private const PATTERN = '~\* @testedClass (?<targetClass>.+?)(?:\n| \*/)~';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\Tests\TestCheck;
6+
7+
use Cdn77\TestUtils\TestCheck\EveryTestHasSameNamespaceAsCoveredClass;
8+
use Cdn77\TestUtils\Tests\BaseTestCase;
9+
use Generator;
10+
use PHPUnit\Framework\AssertionFailedError;
11+
12+
final class EveryTestHasSameNamespaceAsCoveredClassTest extends BaseTestCase
13+
{
14+
/** @dataProvider providerSuccess */
15+
public function testSuccess(string $filePath): void
16+
{
17+
$check = new EveryTestHasSameNamespaceAsCoveredClass(
18+
[__DIR__ . '/Fixtures/EveryTestHasSameNamespaceAsCoveredClass/tests/' . $filePath],
19+
'Tests'
20+
);
21+
$check->run($this);
22+
}
23+
24+
/** @return Generator<array-key, list<string>> */
25+
public function providerSuccess(): Generator
26+
{
27+
$files = [
28+
'IgnoreMultipleCoversTest.php',
29+
'SameNamespaceTest.php',
30+
'SameNamespaceAsLinkedCoveredClassTest.php',
31+
'CoveredClassWithSomeWhitespaceTest.php',
32+
'CoversNothingTest.php',
33+
'CoversDefaultClassTest.php',
34+
];
35+
36+
foreach ($files as $file) {
37+
yield $file => [$file];
38+
}
39+
}
40+
41+
/** @dataProvider providerFail */
42+
public function testFail(string $filePath, string $error): void
43+
{
44+
$this->expectException(AssertionFailedError::class);
45+
$this->expectExceptionMessage($error);
46+
47+
$check = new EveryTestHasSameNamespaceAsCoveredClass(
48+
[__DIR__ . '/Fixtures/EveryTestHasSameNamespaceAsCoveredClass/tests/' . $filePath],
49+
'Tests'
50+
);
51+
$check->run($this);
52+
}
53+
54+
/** @return Generator<array-key, list<string>> */
55+
public function providerFail(): Generator
56+
{
57+
yield [
58+
'CoversNonexistentClassTest.php',
59+
'is pointing to an non-existing class',
60+
];
61+
62+
yield [
63+
'CoversAndCoversNothingTest.php',
64+
'contains both @covers and @coversNothing annotations',
65+
];
66+
67+
yield [
68+
'SubNamespace/SameNamespaceTest.php',
69+
'is in the wrong namespace',
70+
];
71+
72+
yield [
73+
'SameNamespaceWrongNameTest.php',
74+
'is in the wrong namespace',
75+
];
76+
}
77+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;
6+
7+
final class SameNamespace
8+
{
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;
6+
7+
/** @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass\SameNamespace */
8+
final class CoveredClassWithSomeWhitespaceTest
9+
{
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;
6+
7+
/**
8+
* @coversNothing
9+
* @covers \stdClass
10+
*/
11+
final class CoversAndCoversNothingTest
12+
{
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;
6+
7+
// phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
8+
/** @coversDefaultClass Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass\SameNamespace */
9+
final class CoversDefaultClassTest
10+
{
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cdn77\TestUtils\Tests\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsCoveredClass;
6+
7+
/** @covers Cdn77\TestUtils\Tests\TestCheck\Fixtures\EveryTestHasSameNamespaceAsTestedClass\Noexists */
8+
final class CoversNonexistentClassTest
9+
{
10+
}

0 commit comments

Comments
 (0)