Skip to content

Commit 12c9a34

Browse files
committed
Expose a single way of writing architectural tests
The package offered three overlapping entry points: an ArchRuleTestCase base class, an ArchRuleAsserts trait, and the raw constraint. The base class was a pure duplicate of the trait, and having both invited the wrong choice: it burns the parent class, so anyone already extending KernelTestCase or a project base class had to fall back to the trait anyway. Keep the trait, which works in every case, and drop the base class. Drop assertArchRules() too. It was sugar over a loop that reparsed the whole class set once per rule, and one rule per test method reads better anyway: PHPUnit names the broken rule and still reports the ones that pass. If a multi-rule assertion is wanted later it should aggregate into a single parsing pass, which is a different design rather than an extension of this one. The constraint stays public: it is the class the package was extracted from, and it is what the trait is built on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FE8ESEV7TzGLDwJuTfdq95
1 parent 62c361e commit 12c9a34

6 files changed

Lines changed: 36 additions & 171 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
`Arkitect\PHPUnit` namespace are unchanged, so existing usages keep working.
1515
- An optional target PHP version argument on the constraint constructor, so the analyzer can parse
1616
code for a PHP version other than the one running the tests.
17-
- `ArchRuleAsserts`, a trait providing `assertArchRule()` and `assertArchRules()`. It used to live
18-
in the core repository's end-to-end tests, where it was not installable.
19-
- `ArchRuleTestCase`, a PHPUnit `TestCase` with the trait already applied.
17+
- `ArchRuleAsserts`, a trait providing `assertArchRule()`. It is the single entry point of the
18+
package. The helper used to live in the core repository's end-to-end tests, where nobody could
19+
install it. It is a trait rather than a base test case so that it also works when the parent
20+
class is already taken by a framework.
2021
- Support for PHPUnit 12, alongside 9.6, 10 and 11.
2122

2223
### Changed

README.md

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ This package lets you do the same thing from inside your test suite instead: a b
1717
rule becomes a failing test, with the violations rendered in the failure message.
1818

1919
```php
20-
final class ArchitectureTest extends ArchRuleTestCase
20+
final class ArchitectureTest extends TestCase
2121
{
22+
use ArchRuleAsserts;
23+
2224
public function test_controllers_are_suffixed_properly(): void
2325
{
2426
$rule = Rule::allClasses()
@@ -42,12 +44,7 @@ Composer pulls in whatever it needs.
4244

4345
## Usage
4446

45-
There are three ways to use the bridge, in increasing order of control.
46-
47-
### 1. Extend `ArchRuleTestCase`
48-
49-
The shortest path. `ArchRuleTestCase` extends PHPUnit's `TestCase` and adds the architectural
50-
assertions:
47+
Add the `ArchRuleAsserts` trait to a test class and call `assertArchRule()`. That is the whole API:
5148

5249
```php
5350
<?php
@@ -59,11 +56,14 @@ namespace App\Tests;
5956
use Arkitect\ClassSet;
6057
use Arkitect\Expression\ForClasses\NotHaveDependencyOutsideNamespace;
6158
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
62-
use Arkitect\PHPUnit\ArchRuleTestCase;
59+
use Arkitect\PHPUnit\ArchRuleAsserts;
6360
use Arkitect\Rules\Rule;
61+
use PHPUnit\Framework\TestCase;
6462

65-
final class ArchitectureTest extends ArchRuleTestCase
63+
final class ArchitectureTest extends TestCase
6664
{
65+
use ArchRuleAsserts;
66+
6767
public function test_the_domain_does_not_depend_on_the_framework(): void
6868
{
6969
$rule = Rule::allClasses()
@@ -76,64 +76,38 @@ final class ArchitectureTest extends ArchRuleTestCase
7676
}
7777
```
7878

79-
### 2. Use the `ArchRuleAsserts` trait
80-
81-
If your tests already extend a base class of your own, pull the assertions in with the trait:
82-
83-
```php
84-
final class ArchitectureTest extends MyProjectTestCase
85-
{
86-
use ArchRuleAsserts;
87-
88-
public function test_controllers_are_suffixed_properly(): void
89-
{
90-
self::assertArchRule($rule, ClassSet::fromDir(__DIR__.'/../src'));
91-
}
92-
}
93-
```
94-
95-
Both entry points expose the same two assertions:
96-
9779
| Assertion | Description |
9880
| --- | --- |
9981
| `assertArchRule(ArchRule $rule, ClassSet $classSet, string $message = '')` | Asserts that every class in the set satisfies the rule. |
100-
| `assertArchRules(array $rules, ClassSet $classSet, string $message = '')` | Asserts a list of rules against the same set, failing on the first one that is violated. |
10182

102-
Checking several rules against one class set is the common case, and `assertArchRules` keeps it to a
103-
single assertion:
83+
One rule per test method keeps the failures readable: PHPUnit names the broken rule for you, and
84+
the report tells you which rules still pass. Checking several rules in one method works too, it
85+
just collapses them into a single pass/fail.
86+
87+
It is a trait and not a base test case so that it also works where the parent class is already
88+
taken — `KernelTestCase`, `WebTestCase`, or your own project base class:
10489

10590
```php
106-
public function test_the_layers_are_respected(): void
91+
final class ArchitectureTest extends KernelTestCase
10792
{
108-
self::assertArchRules(
109-
[
110-
Rule::allClasses()
111-
->that(new ResideInOneOfTheseNamespaces('App\Domain'))
112-
->should(new NotHaveDependencyOutsideNamespace('App\Domain'))
113-
->because('the domain must stay framework agnostic'),
114-
Rule::allClasses()
115-
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
116-
->should(new HaveNameMatching('*Controller'))
117-
->because('it makes the codebase easier to navigate'),
118-
],
119-
ClassSet::fromDir(__DIR__.'/../src')
120-
);
93+
use ArchRuleAsserts;
94+
95+
// ...
12196
}
12297
```
12398

124-
### 3. Use the constraint directly
99+
### Under the hood
125100

126-
`ArchRuleCheckerConstraintAdapter` is a plain PHPUnit constraint, so it composes with
127-
`assertThat()` and anything else that takes a `Constraint`:
101+
`assertArchRule()` is a thin wrapper over `ArchRuleCheckerConstraintAdapter`, a plain PHPUnit
102+
constraint. It is part of the public API — it is the class this package was extracted from — so you
103+
can use it directly if you need to compose it with `assertThat()` or another constraint:
128104

129105
```php
130-
use Arkitect\PHPUnit\ArchRuleCheckerConstraintAdapter;
131-
132106
self::assertThat($rule, new ArchRuleCheckerConstraintAdapter($classSet));
133107
```
134108

135-
The constraint is statefulit holds the violations collected while matching so it can render
136-
them in the failure message. Build a fresh one for every assertion.
109+
The constraint is stateful: it holds the violations collected while matching so it can render them
110+
in the failure message. Build a fresh one for every assertion.
137111

138112
## Failure output
139113

src/ArchRuleAsserts.php

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
use PHPUnit\Framework\Assert;
1010

1111
/**
12-
* Adds architectural assertions to any test case.
12+
* Adds architectural assertions to a test case.
1313
*
14-
* Use this trait when your test already extends a base class of your own;
15-
* otherwise extend ArchRuleTestCase, which pulls the trait in for you.
14+
* This is the entry point of the package: use it in any test class, whatever it
15+
* extends. It is a trait rather than a base test case on purpose, so it also works
16+
* where the parent class is already taken by a framework (KernelTestCase and friends).
1617
*/
1718
trait ArchRuleAsserts
1819
{
@@ -23,21 +24,4 @@ public static function assertArchRule(ArchRule $rule, ClassSet $classSet, string
2324
{
2425
Assert::assertThat($rule, new ArchRuleCheckerConstraintAdapter($classSet), $message);
2526
}
26-
27-
/**
28-
* Asserts that every class in $classSet satisfies all the given rules.
29-
*
30-
* Each rule is checked against a fresh constraint, so the failure message
31-
* points at the first rule that is not satisfied.
32-
*
33-
* @param list<ArchRule> $rules
34-
*/
35-
public static function assertArchRules(array $rules, ClassSet $classSet, string $message = ''): void
36-
{
37-
Assert::assertNotEmpty($rules, 'No architectural rule was given to assert.');
38-
39-
foreach ($rules as $rule) {
40-
self::assertArchRule($rule, $classSet, $message);
41-
}
42-
}
4327
}

src/ArchRuleTestCase.php

Lines changed: 0 additions & 25 deletions
This file was deleted.

tests/ArchRuleAssertsTest.php

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Arkitect\ClassSet;
88
use Arkitect\Expression\ForClasses\HaveNameMatching;
99
use Arkitect\Expression\ForClasses\Implement;
10-
use Arkitect\Expression\ForClasses\NotHaveDependencyOutsideNamespace;
1110
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
1211
use Arkitect\PHPUnit\ArchRuleAsserts;
1312
use Arkitect\Rules\DSL\ArchRule;
@@ -19,57 +18,27 @@ class ArchRuleAssertsTest extends TestCase
1918
{
2019
use ArchRuleAsserts;
2120

22-
public function test_assert_arch_rule_passes_on_a_satisfied_rule(): void
21+
public function test_it_passes_on_a_satisfied_rule(): void
2322
{
2423
self::assertArchRule(self::satisfiedRule(), self::mvcClassSet());
2524
}
2625

27-
public function test_assert_arch_rule_fails_on_a_violated_rule(): void
26+
public function test_it_fails_on_a_violated_rule(): void
2827
{
2928
$this->expectException(ExpectationFailedException::class);
3029
$this->expectExceptionMessage('should implement App\ContainerAwareInterface because i said so');
3130

3231
self::assertArchRule(self::violatedRule(), self::mvcClassSet());
3332
}
3433

35-
public function test_assert_arch_rule_prepends_the_custom_message(): void
34+
public function test_it_prepends_the_custom_message_to_the_failure(): void
3635
{
3736
$this->expectException(ExpectationFailedException::class);
3837
$this->expectExceptionMessage('controllers must be container aware');
3938

4039
self::assertArchRule(self::violatedRule(), self::mvcClassSet(), 'controllers must be container aware');
4140
}
4241

43-
public function test_assert_arch_rules_passes_when_every_rule_is_satisfied(): void
44-
{
45-
self::assertArchRules(
46-
[
47-
self::satisfiedRule(),
48-
Rule::allClasses()
49-
->that(new ResideInOneOfTheseNamespaces('App\Controller'))
50-
->should(new NotHaveDependencyOutsideNamespace('App'))
51-
->because('controllers should not leak outside App'),
52-
],
53-
self::mvcClassSet()
54-
);
55-
}
56-
57-
public function test_assert_arch_rules_fails_on_the_first_violated_rule(): void
58-
{
59-
$this->expectException(ExpectationFailedException::class);
60-
$this->expectExceptionMessage('should implement App\ContainerAwareInterface because i said so');
61-
62-
self::assertArchRules([self::satisfiedRule(), self::violatedRule()], self::mvcClassSet());
63-
}
64-
65-
public function test_assert_arch_rules_rejects_an_empty_rule_list(): void
66-
{
67-
$this->expectException(ExpectationFailedException::class);
68-
$this->expectExceptionMessage('No architectural rule was given to assert.');
69-
70-
self::assertArchRules([], self::mvcClassSet());
71-
}
72-
7342
private static function mvcClassSet(): ClassSet
7443
{
7544
return ClassSet::fromDir(__DIR__.'/_fixtures/mvc');

tests/ArchRuleTestCaseTest.php

Lines changed: 0 additions & 38 deletions
This file was deleted.

0 commit comments

Comments
 (0)