Skip to content

Commit 3ad7191

Browse files
SirCotareclaude
andcommitted
make the opt-outs usable once, and close the blacklist hole
Addresses the review on #4, all three points verified against reverts. The opt-outs are read with areNotMetaAnnotatedWith / isMetaAnnotatedWith instead of the direct-only variants, so a project carries @ArchIgnoreNoProductionCounterpart on one stereotype of its own rather than repeating it on 74 classes. ArchUnit counts a direct annotation as meta-annotated, so annotating a single class still works. @ArchIgnoreGroupName gets the same treatment in all three places it is read: it is the sibling opt-out with the identical ergonomics problem, and leaving it direct-only would be half a fix. Architecture tests are exempted from the production-counterpart rule by package, via .._architecture.. alongside .._support.. / .._config.., so dropping the hardcoded "ArchitectureTest" name does not push boilerplate onto every consumer. Deliberately not added to TestClassVisibilityArchRule: being package private is as achievable for an architecture test as for any other test, so the two exclusion lists encode different facts and are meant to differ. org.junit.jupiter.api.Assertions.assertThrowsExactly is blacklisted again. It only ever existed under the AssertJ namespace that the previous commit removed, so the cleanup dropped the house rule along with the bogus entry. Now covered by a fixture that actually calls it, not only by a list assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 383cf46 commit 3ad7191

28 files changed

Lines changed: 297 additions & 12 deletions

readme.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,26 @@ Two annotations exempt a class from a specific rule. Neither is meta-annotated w
7373
| `@ArchIgnoreGroupName` | a `@Nested` test class | needing a production method of the same name, for a class that only groups tests |
7474

7575
Use `@ArchIgnoreNoProductionCounterpart` for a test named after the behaviour it describes rather than
76-
after a production class, and on your own `ArchitectureTest`.
76+
after a production class.
77+
78+
Both are read as meta-annotations, so a project declares its intent once on its own stereotype instead
79+
of repeating the annotation on every class:
80+
81+
```java
82+
83+
@Target(ElementType.TYPE)
84+
@Retention(RetentionPolicy.RUNTIME)
85+
@ArchIgnoreNoProductionCounterpart
86+
public @interface BusinessTest {
87+
}
88+
```
89+
90+
Annotating a single class directly still works — ArchUnit counts a direct annotation as
91+
meta-annotated.
92+
93+
Architecture tests need neither: any class in a package named `_architecture` is exempt from the
94+
production-counterpart rule, alongside the existing `_support` and `_config` exclusions. Use the
95+
annotation for the one-off that lives elsewhere.
7796

7897
## Local Development
7998

src/main/java/it/aboutbits/archunit/toolbox/rule/base/BlacklistMethodsArchRule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public interface BlacklistMethodsArchRule {
2424
// We should use `assertThatExceptionOfType(...).isThrownBy(...)` instead of `assertThatThrownBy(...)`
2525
"org.assertj.core.api.Assertions.assertThatThrownBy",
2626
"org.junit.jupiter.api.Assertions.assertThrows",
27+
"org.junit.jupiter.api.Assertions.assertThrowsExactly",
2728
"org.junit.jupiter.api.Assertions.assertDoesNotThrow",
2829
// assertThat (allowed is only org.assertj.core.api.Assertions.assertThat)
2930
"org.assertj.core.api.AssertionsForClassTypes.assertThat",

src/main/java/it/aboutbits/archunit/toolbox/rule/base/TestClassInCorrectPackageArchRule.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,24 @@ public interface TestClassInCorrectPackageArchRule {
1919
default void test_classes_should_be_in_the_same_package_as_their_production_code(JavaClasses classes) {
2020
classes().that(TestClassNames.testClasses())
2121
.and()
22-
.areNotAnnotatedWith(org.junit.jupiter.api.Disabled.class)
22+
.areNotMetaAnnotatedWith(org.junit.jupiter.api.Disabled.class)
2323
.and()
24-
.areNotAnnotatedWith(com.tngtech.archunit.junit.ArchIgnore.class)
24+
.areNotMetaAnnotatedWith(com.tngtech.archunit.junit.ArchIgnore.class)
2525
.and()
26-
.areNotAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreNoProductionCounterpart.class)
26+
/*
27+
* Meta-annotated, not annotated: a project marks its scenario tests with one
28+
* stereotype of its own that carries this annotation, rather than repeating the
29+
* annotation on every class. ArchUnit counts a direct annotation as meta-annotated,
30+
* so annotating a single class still works.
31+
*/
32+
.areNotMetaAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreNoProductionCounterpart.class)
2733
.and()
28-
.resideOutsideOfPackages(".._support..", ".._config..")
34+
/*
35+
* An architecture test is named after no production class by definition. Excluded by
36+
* package here, but deliberately not in TestClassVisibilityArchRule: being package
37+
* private is just as achievable for an architecture test as for any other test.
38+
*/
39+
.resideOutsideOfPackages(".._support..", ".._config..", ".._architecture..")
2940
.should(new BeInTheSamePackageAsTheProductionClass(classes))
3041
.allowEmptyShould(true)
3142
.check(classes);

src/main/java/it/aboutbits/archunit/toolbox/rule/base/TestNestedClassMatchNameArchRule.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ public interface TestNestedClassMatchNameArchRule {
2323
default void nested_test_classes_have_matching_production_method_name(JavaClasses classes) {
2424
classes().that(TestClassNames.testClasses())
2525
.and()
26-
.areNotAnnotatedWith(org.junit.jupiter.api.Disabled.class)
26+
.areNotMetaAnnotatedWith(org.junit.jupiter.api.Disabled.class)
2727
.and()
28-
.areNotAnnotatedWith(com.tngtech.archunit.junit.ArchIgnore.class)
28+
.areNotMetaAnnotatedWith(com.tngtech.archunit.junit.ArchIgnore.class)
2929
.and()
3030
/*
3131
* A test class that declares it has no production counterpart has no production
3232
* methods to match its @Nested classes against either.
3333
*/
34-
.areNotAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreNoProductionCounterpart.class)
34+
.areNotMetaAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreNoProductionCounterpart.class)
3535
.should(new HaveNestedClassesThatHaveAMatchingProductionMethodName(classes))
3636
.allowEmptyShould(true)
3737
.check(classes);
@@ -53,7 +53,7 @@ public void check(JavaClass testClass, ConditionEvents events) {
5353
.stream()
5454
.filter(clazz -> clazz.getName().startsWith(testClass.getName() + "$")
5555
&& clazz.isAnnotatedWith(org.junit.jupiter.api.Nested.class)
56-
&& !clazz.isAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName.class)
56+
&& !clazz.isMetaAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName.class)
5757
&& !clazz.getName().endsWith("$Validation")
5858
)
5959
.collect(Collectors.toSet());
@@ -83,7 +83,7 @@ public void check(JavaClass testClass, ConditionEvents events) {
8383
.stream()
8484
.anyMatch(clazz -> clazz.getName().startsWith(nestedClass.getName() + "$")
8585
&& clazz.isAnnotatedWith(org.junit.jupiter.api.Nested.class)
86-
&& !clazz.isAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName.class)
86+
&& !clazz.isMetaAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName.class)
8787
&& !clazz.getName().endsWith("$Validation")
8888
)
8989
) {

src/main/java/it/aboutbits/archunit/toolbox/rule/common/ControllerRequestMappingsMustBeSecurityTested.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ public void check(JavaMethod method, ConditionEvents events) {
9191
.stream()
9292
.anyMatch(clazz -> isExpectedNestedClass(clazz.getName(), expectedNestedClassName)
9393
&& clazz.isAnnotatedWith(org.junit.jupiter.api.Nested.class)
94-
&& !clazz.isAnnotatedWith(com.tngtech.archunit.junit.ArchIgnore.class)
95-
&& !clazz.isAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName.class)
94+
&& !clazz.isMetaAnnotatedWith(com.tngtech.archunit.junit.ArchIgnore.class)
95+
&& !clazz.isMetaAnnotatedWith(it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName.class)
9696
);
9797

9898
if (!nestedMethodTestClassFound) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package it.aboutbits.archunit.fixture.blacklistmethods.badjunitassertion;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
5+
public class CallsJunitAssertThrowsExactly {
6+
public void check() {
7+
Assertions.assertThrowsExactly(IllegalStateException.class, () -> {
8+
throw new IllegalStateException();
9+
});
10+
}
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package it.aboutbits.archunit.fixture.nestedclassname.goodmetagroup;
2+
3+
import it.aboutbits.archunit.toolbox.support.ArchIgnoreGroupName;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/// A project's own marker for a purely organisational @Nested class, carrying the opt-out as a
11+
/// meta-annotation.
12+
@Target(ElementType.TYPE)
13+
@Retention(RetentionPolicy.RUNTIME)
14+
@ArchIgnoreGroupName
15+
public @interface TestGroup {
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package it.aboutbits.archunit.fixture.nestedclassname.goodmetagroup;
2+
3+
public class Widget {
4+
public void doWork() {
5+
}
6+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package it.aboutbits.archunit.fixture.nestedclassname.goodmetagroup;
2+
3+
import org.junit.jupiter.api.Nested;
4+
5+
class WidgetTest {
6+
/// Matches Widget.doWork(), so the rule has a nested class to actually check.
7+
@Nested
8+
class DoWork {
9+
}
10+
11+
/// Groups tests only. Widget has no someGrouping() method, and must not be expected to.
12+
@Nested
13+
@TestGroup
14+
class SomeGrouping {
15+
}
16+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package it.aboutbits.archunit.fixture.nestedclassname.goodmetaoptout;
2+
3+
import it.aboutbits.archunit.toolbox.support.ArchIgnoreNoProductionCounterpart;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/// A project's own test stereotype, carrying the opt-out as a meta-annotation.
11+
@Target(ElementType.TYPE)
12+
@Retention(RetentionPolicy.RUNTIME)
13+
@ArchIgnoreNoProductionCounterpart
14+
public @interface BusinessScenario {
15+
}

0 commit comments

Comments
 (0)