ArchUnit とは
- Java プログラムのアーキテクチャをユニットテストするためのライブラリ
- パッケージやクラス、レイヤ間の依存関係をチェックしたり、循環参照をチェックしたりできる
- 読みは「あーきゆにっと」
環境
Java
>java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.8+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.8+10, mixed mode)
OS
Windows 10 Home 64bit
JUnit
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter:5.7.0"
...
}
Hello World
実装
|-build.gradle
`-src/
|-main/java/
| `-example/
| |-controller/
| | `-FooController.java
| `-service/
| `-FooService.java
|
`-test/java/
`-example/
`-ArchUnitTest.java
plugins {
id "java"
}
sourceCompatibility = 11
targetCompatibility = 11
[compileJava, compileTestJava]*.options*.encoding = "UTF-8"
repositories {
mavenCentral()
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter:5.7.0"
testImplementation "com.tngtech.archunit:archunit-junit5:0.14.1"
}
test {
useJUnitPlatform()
}
package example.service;
import example.controller.FooController;
public class FooService {
// Service が Controller に依存した状態になっている
FooController controller;
}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example");
ArchRule rule = noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
rule.check(javaClasses);
}
}
実行結果
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..service..' should depend on classes that reside in a package '..controller..'' was violated (1 times):
Field <example.service.FooService.controller> has type <example.controller.FooController> in (FooService.java:0)
説明
依存関係
dependencies {
...
testImplementation "com.tngtech.archunit:archunit-junit5:0.14.1"
}
- JUnit5 で ArchUnit を使う場合は、
com.tngtech.archunit:archunit-junit5
を依存関係に追加する
クラスをインポートする
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
...
import org.junit.jupiter.api.Test;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example");
...
}
}
- ArchUnit のテストは、まずクラスをインポートすることから始める
- インポートには
ClassFileImporter
を使用する - 上記例の
importPackages(String)
は、指定したパッケージ以下のクラスを再帰的に読み込んでJavaClasses
型で返している-
JavaClasses
には、読み込まれたクラスが含まれている
-
アーキテクチャのルールを定義する
package example;
...
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
...
ArchRule rule = noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
...
}
}
- アーキテクチャのルール(検証内容)は、
ArchRule
という型で定義する -
ArchRule
を作るための API として、ArchRuleDefinition
に静的なファクトリメソッドが用意されている- ここでは
noClasses()
でArchRule
の定義を開始している
- ここでは
-
ArchRule
の定義は流れるようなインタフェースで構築できるようになっている-
noClasses()
は、このあとに続くルールに該当するクラスが存在しないことを定義している -
resideInAPackage(String)
は、パッケージの範囲を絞り込んでいる- 2つのドット(
..
)は、任意のサブパッケージを表している - したがって
..service..
は、すべてのservice
という名前のパッケージ以下の、すべてのサブパッケージを対象としている
- 2つのドット(
-
dependOnClassesThat()
は、should()
より前で指定した対象が依存する先を定義している-
.should().dependOnClassesThat().resideInAPackage("..controller..")
は、controller
パッケージ以下のクラスに依存しなければならないことを宣言している
-
- 以上の宣言が組み合わさることで、「
service
パッケージ以下には、controller
パッケージ以下のクラスに依存しているクラスが存在しない」ことをルールとして定義している-
noClasses()
なので、後ろのルールに該当するクラスが存在しないことを定義していることになる
-
-
ルールをチェックする
...
public class ArchUnitTest {
@Test
void test() {
...
rule.check(javaClasses);
}
}
- 作成した
ArchRule
のcheck(JavaClasses)
メソッドに、インポートしたJavaClasses
を渡す - これにより、
JavaClasses
に含まれるクラスがArchRule
で定義されたルールを守っているかどうかを検証できる
3つのAPI
ArchUnit には、次の3つの API が用意されている
- Core API
- Lang API
- Library API
Core API
package example.controller;
import example.service.FooService;
public class FooController {
private final FooService fooService;
public FooController(FooService fooService) {
this.fooService = fooService;
}
public String invoke() {
return fooService.execute();
}
}
package example;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.controller.FooController;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example");
JavaClass fooControllerClass = javaClasses.get(FooController.class);
fooControllerClass.getFields().forEach(field -> {
System.out.println("name=" + field.getName() + ", type=" + field.getRawType());
});
fooControllerClass.getMethods().forEach(method -> {
System.out.println("name=" + method.getName() + ", return type=" + method.getRawReturnType());
});
}
}
name=fooService, type=JavaClass{name='example.service.FooService'}
name=invoke, return type=JavaClass{name='java.lang.String'}
- ArchUnit の最も基本となる API
- JavaClass や JavaMethod のような、 Java プログラムの要素を表す型で構成されている
- リフレクション API と同じように、クラスのメタ情報を参照できる
- さらに、クラスの参照情報なども取得できるようになっている
package example;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.service.FooService;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example");
JavaClass fooServiceClass = javaClasses.get(FooService.class);
fooServiceClass.getDirectDependenciesToSelf().forEach(System.out::println);
}
}
Dependency{
originClass=JavaClass{name='example.controller.FooController'},
targetClass=JavaClass{name='example.service.FooService'},
lineNumber=13,
description=Method <example.controller.FooController.invoke()> calls method <example.service.FooService.execute()> in (FooController.java:13)
}
Dependency{
originClass=JavaClass{name='example.controller.FooController'},
targetClass=JavaClass{name='example.service.FooService'},
lineNumber=0,
description=Field <example.controller.FooController.fooService> has type <example.service.FooService> in (FooController.java:0)
}
Dependency{
originClass=JavaClass{name='example.controller.FooController'},
targetClass=JavaClass{name='example.service.FooService'},
lineNumber=0,
description=Constructor <example.controller.FooController.<init>(example.service.FooService)> has parameter of type <example.service.FooService> in (FooController.java:0)
}
-
getDirectDependenciesToSelf()
により、FooService
を直接参照している場所を取得している - このように、 Core API は解析対象となる Java プログラムの情報を参照するための、最も低レベルな API を提供している
Core API は抽象度が低い
- 理屈上は、この Core API を使うことで Java プログラムの構造を解析し、意図したアーキテクチャ・ルールを守れているかをチェックできる
- しかし、そのためにはプログラムの情報を抽出して検証する処理を自力で実装する必要があり、かなり大変な作業になる
- Core API は最も基本的な API のため何でもできるが、「チェックしたいアーキテクチャ・ルール」を記述するには抽象度が低すぎる
- そこで、 Core API をラップしてより抽象度の高い記述をできるようにした Lang API が用意されている
主要クラスの型階層
- Core API に含まれる Java コードの構造を表す主要なクラスは、上図のような関係になっている
-
JavaMember
はメソッド・コンストラクタ・フィールドをまとめたものを表し、
JavaCodeUnit
はメソッドとコンストラクタをまとめたものを表している -
JavaClass
は、JavaMember
とは独立した存在となっている - その他のクラスも含めた完全な図は ユーザーガイドの図 を参照
Lang API
package example;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class Main {
public static void main(String[] args) {
ArchRule rule = noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat().resideInAPackage("..controller..");
System.out.println(rule.getDescription());
}
}
no classes that reside in a package '..service..' should depend on classes that reside in a package '..controller..'
- Lang API は、前述の Core API よりも抽象度の高い記述でアーキテクチャのルールを定義できる API となっている
-
ArchRuleDefinition
のファクトリメソッドを起点として、流れるようなインタフェースでルールを記述できる
Library API
package example;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.library.Architectures.*;
public class Main {
public static void main(String[] args) {
ArchRule rule = layeredArchitecture()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");
System.out.println(rule.getDescription());
}
}
Layered architecture consisting of
layer 'Controller' ('..controller..')
layer 'Service' ('..service..')
layer 'Persistence' ('..persistence..')
where layer 'Controller' may not be accessed by any layer
where layer 'Service' may only be accessed by layers ['Controller']
where layer 'Persistence' may only be accessed by layers ['Service']
- Library API は、特定のアーキテクチャパターンに関するルールを、より簡潔に記述できるようにした API を提供している
- ここでは、レイヤードアーキテクチャのレイヤーを定義し、レイヤー間の依存関係のルールを宣言している
- 2020年11月現在、レイヤードアーキテクチャ以外にもオニオンアーキテクチャ用の API も用意されている
クラスの読み込み
`-src/
|-main/java/
| `-example/archunit/
| |-Hoge.java
| |-foo/
| | `-Foo.java
| `-bar/
| `-Bar.java
`-test/java/
`-example/
`-Main.java
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");
javaClasses.forEach(System.out::println);
}
}
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.archunit.foo.Foo'}
JavaClass{name='example.archunit.Hoge'}
- ArchUnit を使うためには、まずは検証対象となるクラスを読み込む必要がある
- クラスの読み込みには、 ClassFileImporter を使用する
- importPackages(String...) メソッドを使用すると、引数で指定したパッケージ以下を再帰的に探索してクラスを読み込むことができる
- 読み込み結果は JavaClasses という型で返される
- クラスは、実行時のクラスパスから読み込まれる
- したがって、テスト実行時などは
src/test/java
以下のクラスも読み込み対象になるので注意が必要- テストクラスの読み込みを避けるには ImportOption を利用する
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example"); // 対象を example パッケージに変更
javaClasses.forEach(System.out::println);
}
}
JavaClass{name='example.archunit.foo.Foo'}
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.Main'}
JavaClass{name='example.archunit.Hoge'}
-
src/test/java
フォルダ以下のMain
クラスも読み込まれている
パス指定で読み込む
|-build.gradle
|-build/
| |-classes/java/main/
| : `-example/archunit/
| :
`-src/
- Gradle でコンパイルすると、
src/main/java
以下のソースのコンパイル結果はbuild/classes/java/main
以下に出力される - この場所を指定して読み込むこともできる
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPath("build/classes/java/main");
javaClasses.forEach(System.out::println);
}
}
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.archunit.foo.Foo'}
JavaClass{name='example.archunit.Hoge'}
- importPath(String) メソッドを使うと、引数で指定したパスからクラスファイルを読み込む
その他の読み込み方
上記以外にも、特定のクラスだけを読み込んだり、 URL
指定で読み込む方法なども提供されている。
詳細は、 ClassFileImporter の Javadoc を参照。
読み込み対象の場所を絞る
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter()
.withImportOption(location -> location.contains("/foo/") || location.contains("/bar/"))
.importPackages("example.archunit");
javaClasses.forEach(System.out::println);
}
}
JavaClass{name='example.archunit.bar.Bar'}
JavaClass{name='example.archunit.foo.Foo'}
-
withImportOption(ImportOption) を使用すると、読み込み対象となる場所を任意に絞ることができる
-
withImportOption()
は、ImportOption
を置き換えた新しいClassFileImporter
インスタンスを返す - 元の
ClassFileImport
インスタンスはそのままなので注意
-
-
ImportOption の includes(Location) メソッドには、現在読み込もうとしている場所が Location オブジェクトで渡されるので、この場所を対象とするかどうかを
boolean
で返すように実装する
組み込みの ImportOption
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("example");
javaClasses.forEach(System.out::println);
}
}
-
ImportOption.Predefined に、よく使いそうな
ImportOption
の定数が用意されている -
DO_NOT_INCLUDE_ARCHIVES
- アーカイブを対象外にする
- アーカイブと jar の違いは Location.isArchive() で説明されている
- JAR ファイルと、Java 9 で追加された JRT ファイルとかいうのを含んだものをアーカイブと呼んでいる
-
DO_NOT_INCLUDE_JARS
- JAR ファイルを対象外にする
-
DO_NOT_INCLUDE_TESTS
- テストコードを対象外にする
- 具体的には、
Location
の場所が次のいずれかの正規表現にマッチすると対象外になる.*/target/test-classes/.*
.*/build/classes/([^/]+/)?test/.*
.*/out/test/classes/.*
依存するクラスの自動的な読み込み
`-example/archunit/
|-fizz/
| |-Hoge.java
| `-Fuga.java
`-buzz/
|-Foo.java
`-Bar.java
package example.archunit.fizz;
@Deprecated
public class Hoge {}
package example.archunit.fizz;
public class Fuga {}
package example.archunit.buzz;
import example.archunit.fizz.Hoge;
public class Foo {
Hoge hoge = new Hoge();
}
package example.archunit.buzz;
import example.archunit.fizz.Fuga;
public class Bar {
Fuga fuga = new Fuga();
}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.buzz");
javaClasses.forEach(System.out::println);
final ArchRule rule = noClasses()
.should().accessClassesThat().areAnnotatedWith(Deprecated.class);
rule.check(javaClasses);
}
}
-
example.archunit.buzz
パッケージを読み込んで、@Deprecated
で注釈されたクラスにアクセスしているクラスが存在しないことをチェックしている
JavaClass{name='example.archunit.buzz.Bar'}
JavaClass{name='example.archunit.buzz.Foo'}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes should access classes that are annotated with @Deprecated' was violated (1 times):
Constructor <example.archunit.buzz.Foo.<init>()> calls constructor <example.archunit.fizz.Hoge.<init>()> in (Foo.java:6)
-
ClassFileImporter
で読み込んだクラスはFoo
,Bar
の2つだけになっている(Hoge
クラスは読み込まれていない) - しかし、
Hoge
クラスが@Deprecated
で注釈されており、制約に違反していることは判定できている - このように、読み込んだクラスが依存しているクラスは、たとえインポート対象に入っていなくても自動的に読み込まれるようになっている
- この仕組みのおかげで、インポート対象の指定は検証対象だけを意識した設定で済むようになっている
- しかし、もしこの自動的な読み込みがパフォーマンスに影響を与えるような場合は、次のようにして自動的な読み込みをオフにすることもできる
- 自動的にインポートされるクラスが大量にあるが、それらが検証で利用されていないようなケース
自動的な読み込みをオフにする
resolveMissingDependenciesFromClassPath=false
- テスト実行時のクラスパス直下に
archunit.properties
というファイルを配置する - ここに、
resolveMissingDependenciesFromClassPath=false
と設定する - すると、依存先の自動的な読み込み機能がオフになる
- 上と同じテストを、このプロパティファイルを配置した状態で実行すると、結果は下のようになる
JavaClass{name='example.archunit.buzz.Bar'}
JavaClass{name='example.archunit.buzz.Foo'}
Process finished with exit code 0
- テストは正常終了した
- 自動読み込みがオフになっている場合、インポート先に指定していないクラスは、自動的にスタブに置き換えられる
- この場合、
Hoge
クラスがスタブに置き換えられて解析が行われている
- この場合、
- スタブは要はハリボテなので、オリジナルのクラスが持つ特徴(
@Deprecated
で注釈されていること)は反映されていない - したがって、テストは通ってしまっている
- 自動読み込みの機能をオフにする場合は、依存先のクラスもインポート対象に含まれるように注意して指定する必要がある
Lang API の文法
Lang API は、基本的に以下の文法で記述できるようになっている。
<対象> that <対象を絞り込む条件> should <検証する制約>
例えば、 Hello World で書いた Lang API の記述は、次のように読み取ることができる。
// <対象>: 制約を満たす対象が存在しないことを定義
noClasses()
.that()
// <対象を絞り込む条件>: service 以下のパッケージに絞り込み
.resideInAPackage("..service..")
.should()
// <検証する制約>: 「controller パッケージに依存している」という制約を定義
.dependOnClassesThat().resideInAPackage("..controller..");
つまり、「<対象> that <対象を絞り込む条件>
」で検証対象を特定し、「should <検証する制約>
」で検証内容を記述するという形になっている。
条件の追加
package example;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class Main {
public static void main(String[] args) {
ArchRule rule =
noClasses()
.that()
.resideInAPackage("..service..")
.or().resideInAPackage("..persistence..")
.should()
.accessClassesThat().resideInAPackage("..controller..")
.orShould().accessClassesThat().resideInAPackage("..ui..");
System.out.println(rule.getDescription());
}
}
no classes that reside in a package '..service..' or reside in a package '..persistence..' should access classes that reside in a package '..controller..' or should access classes that reside in a package '..ui..'
-
<対象を絞り込む条件>
や<検証する制約>
の部分は、or
やand
で条件を追加できるようになっている
対象の選択
- Lang API で検証対象を選択するには ArchRuleDefinition に定義された
static
なファクトリメソッドを使用する - まずは、分かりやすいものだけを表でまとめる
メソッド | 説明 |
---|---|
classes() |
全型(クラス・インタフェース)を対象にする。 メンバークラス、ローカルクラス、匿名クラスも対象。 |
theClass(Class) , theClass(String)
|
特定の型だけを対象にする。文字列の方は完全名を渡す。 |
constructors() |
全コンストラクタを対象にする。 |
fields() |
全フィールドを対象にする。 |
methods() |
全メソッドを対象にする。 |
members() |
全メンバー(コンストラクタ・フィールド・メソッド)を対象にする。 ここで言っているメンバーとは、主要クラスの型階層で出てきた JavaMember を指しているので、メンバークラスは関係ない。 |
codeUnits() |
全コードユニット(コンストラクタ・メソッド)を対象にする。 |
- さらに、これらを否定するメソッドも用意されている
noClasses()
-
noClass(Class)
,noClass(String)
noConstructors()
noFields()
noMethods()
noMembers()
noCodeUnits()
- これらは、
should()
以後の条件に一致する対象が存在しないことを定義したいときに使用する
対象の絞り込み条件
-
that()
の後ろで指定する、対象を絞り込むためのメソッドについて整理する -
that()
の戻り値の型は、最初に対象を選択したメソッド(classes()
とかmethods()
とか)によって変化する
対象の選択 |
that() が返す型 |
---|---|
classes().that() |
ClassesThat |
members().that() |
MembersThat |
codeUnits().that() |
CodeUnitsThat |
constructors().that() |
CodeUnitsThat (※ConstrutcorsThat は無い) |
methods().that() |
MethodsThat |
fields().that() |
FieldsThat |
-
MembersThat
やCodeUnitThat
は 主要クラスの型階層 と同じ継承関係になっているので、親で定義された絞り込みのメソッドは子のクラスでも使用できる
対象を絞り込むメソッドの一覧
注意事項
-
areNotAnnotatedWith()
のような否定形のメソッドは省略している(基本的にどのメソッドにも否定形のメソッドが用意されている) -
areAnnotatedWith(String)
,areAnnotatedWith(Class)
のようにオーバーロードされているメソッドは、1種類だけ(基本的にClass
型のものだけ)記載している
ClassesThat
メソッド | 説明 |
---|---|
areAnnotatedWith(Class) |
指定されたアノテーションで注釈されている型に絞る |
areAnonymousClasses() |
匿名クラスに絞る |
areAssignableFrom(Class) |
指定された型のインスタンスを代入可能な型に絞る ( 対象型の変数 = 引数の型のインスタンス; ) |
areAssignableTo(Class) |
指定された型に代入可能なクラスに絞る ( 引数の型の変数 = 対象型のインスタンス; ) |
areEnums() |
enumに絞る |
areInnerClasses() |
内部クラスに絞る |
areInterfaces() |
インタフェースに絞る |
areLocalClasses() |
ローカルクラスに絞る |
areMemberClasses() |
メンバークラスに絞る |
areMetaAnnotatedWith(Class) |
指定されたメタアノテーションで注釈されたアノテーションで注釈されている型に絞る。 |
areNestedClasses() |
ネストクラスに絞る |
arePackagePrivate() |
パッケージプライベートな型に絞る |
arePrivate() |
private な型に絞る |
areProtected() |
protected な型に絞る |
arePublic() |
public な型に絞る |
areTopLevelClasses() |
トップレベルクラスに絞る |
belongToAnyOf(Class...) |
指定された型と、その型に属する全てのネストクラスを対象にする |
haveFullyQualifiedName(String) |
完全修飾名が指定された文字列と一致する型に絞る |
haveModifier(JavaModifier) |
指定された修飾子を持つ型に絞る |
haveNameMatching(String) |
完全修飾名が指定された正規表現にマッチする型に絞る |
haveSimpleName(String) |
単純名が指定された文字列と一致する型に絞る |
haveSimpleNameContaining(String) |
単純名に指定された文字列を含む型に絞る |
haveSimpleNameEndingWith(String) |
単純名が指定された文字列で終わる型に絞る |
haveSimpleNameStartingWith(String) |
単純名が指定された文字列で始まる型に絞る |
implement(Class) |
指定されたインタフェースを実装したクラスに絞る |
resideInAnyPackage(String...) |
指定されたいずれかのパッケージに含まれる型に絞る |
resideInAPackage(String) |
指定されたパッケージに含まれる型に絞る |
resideOutsideOfPackage(String) |
指定されたパッケージに含まれない型に絞る |
resideOutsideOfPackages(String...) |
指定されたいずれのパッケージにも含まれない型に絞る |
MembersThat
メソッド | 説明 |
---|---|
areAnnotatedWith(Class) |
指定されたアノテーションで注釈されているメンバーに絞る |
areDeclaredIn(Class) |
指定された型内で宣言されているメンバーに絞る(親の型で宣言されているものは含まない) |
areDeclaredInClassesThat() |
この後で定義する条件を満たすクラスで宣言されているメンバーに絞る |
areMetaAnnotatedWith(Class) |
指定されたメタアノテーションで注釈されたアノテーションで注釈されているメンバーに絞る |
arePackagePrivate() |
パッケージプライベートなメンバーに絞る |
arePrivate() |
private なメンバーに絞る |
areProtected() |
protected なメンバーに絞る |
arePublic() |
public なメンバーに絞る |
haveFullName(String) |
完全名が指定された文字列と一致するメンバーに絞る。 |
haveFullNameMatching(String) |
完全名が指定された正規表現にマッチするメンバーに絞る |
haveModifier(JavaModifier) |
指定された修飾子を持つメンバーに絞る |
haveName(String) |
名前が指定された文字列と一致するメンバーに絞る |
haveNameMatching(String) |
名前が指定された正規表現にマッチするメンバーに絞る |
CodeUnitsThat
メソッド | 説明 |
---|---|
declareThrowableOfType(Class) |
throws句に指定された型の例外を含むコードユニットに絞る |
haveRawParameterTypes(Class...) |
仮引数の型の並びが指定されたものと一致するコードユニットに絞る |
haveRawReturnType(Class) |
戻り値の型が指定されたものと一致するコードユニットに絞る |
MethodsThat
メソッド | 説明 |
---|---|
areFinal() |
final なメソッドに絞る |
areStatic() |
static なメソッドに絞る |
FieldsThat
メソッド | 説明 |
---|---|
areFinal() |
final なフィールドに絞る |
areStatic() |
static なフィールドに絞る |
haveRawType(Class) |
型が指定されたものと一致するフィールドに絞る |
対象を絞り込むメソッドの詳細
実際の動きを見てみないと理解しづらいメソッドに絞って説明。
匿名クラス・内部クラス・ローカルクラス・メンバークラス・ネストクラス・トップレベルクラスの違い
package example.archunit;
public class TopLevelClass {
class MemberClass {}
static class StaticMemberClass {}
interface MemberInterface {}
void method() {
class LocalClass {}
Object anonymousClass = new Object() {};
}
}
package example.archunit;
public class TopLevelInterface {}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");
report("areAnonymousClasses()", javaClasses, classes().that().areAnonymousClasses().should().beAnnotatedWith(Deprecated.class));
report("areInnerClasses()", javaClasses, classes().that().areInnerClasses().should().beAnnotatedWith(Deprecated.class));
report("areLocalClasses()", javaClasses, classes().that().areLocalClasses().should().beAnnotatedWith(Deprecated.class));
report("areMemberClasses()", javaClasses, classes().that().areMemberClasses().should().beAnnotatedWith(Deprecated.class));
report("areNestedClasses()", javaClasses, classes().that().areNestedClasses().should().beAnnotatedWith(Deprecated.class));
report("areTopLevelClasses()", javaClasses, classes().that().areTopLevelClasses().should().beAnnotatedWith(Deprecated.class));
}
void report(String title, JavaClasses javaClasses, ArchRule rule) {
System.out.println("[" + title + "]");
System.out.println(rule.evaluate(javaClasses).getFailureReport() + "\n");
}
}
[areAnonymousClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are anonymous classes should be annotated with @Deprecated' was violated (1 times):
Class <example.archunit.TopLevelClass$1> is not annotated with @Deprecated in (TopLevelClass.java:0)
[areInnerClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are inner classes should be annotated with @Deprecated' was violated (3 times):
Class <example.archunit.TopLevelClass$1> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$1LocalClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
[areLocalClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are local classes should be annotated with @Deprecated' was violated (1 times):
Class <example.archunit.TopLevelClass$1LocalClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
[areMemberClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are member classes should be annotated with @Deprecated' was violated (3 times):
Class <example.archunit.TopLevelClass$MemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberInterface> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$StaticMemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
[areNestedClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are nested classes should be annotated with @Deprecated' was violated (5 times):
Class <example.archunit.TopLevelClass$1> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$1LocalClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$MemberInterface> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelClass$StaticMemberClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
[areTopLevelClasses()]
Architecture Violation [Priority: MEDIUM] - Rule 'classes that are top level classes should be annotated with @Deprecated' was violated (2 times):
Class <example.archunit.TopLevelClass> is not annotated with @Deprecated in (TopLevelClass.java:0)
Class <example.archunit.TopLevelInterface> is not annotated with @Deprecated in (TopLevelInterface.java:0)
整理すると、以下のような関係になっている。
- 以下2つがポイントだと思う
-
areInnerClasses()
はstatic
なクラスは対象外 -
areNestedClasses()
は、トップレベルクラス内で宣言された全てのクラスが対象になる
-
メタアノテーションとは
`-example/archunit/
|-MetaAnnotation.java
|-FooAnnotation.java
|-BarAnnotation.java
|-Foo.java
`-Bar.java
package example.archunit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MetaAnnotation {}
package example.archunit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@MetaAnnotation // ★@MetaAnnotation で注釈されている
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FooAnnotation {}
package example.archunit;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// ★@MetaAnnotation では注釈されていない
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface BarAnnotation {}
package example.archunit;
@FooAnnotation
public class Foo {}
package example.archunit;
@BarAnnotation
public class Bar {}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import example.archunit.MetaAnnotation;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = classes().that().areMetaAnnotatedWith(MetaAnnotation.class)
.should().beAnnotatedWith(Deprecated.class);
rule.check(javaClasses);
}
}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that are meta-annotated with @MetaAnnotation should be annotated with @Deprecated' was violated (1 times):
Class <example.archunit.Foo> is not annotated with @Deprecated in (Foo.java:0)
- メタアノテーションとは、「アノテーションを注釈するアノテーション」のこと
- 上の例では
@MetaAnnotation
がメタアノテーションで、@FooAnnotation
を注釈している -
areMetaAnnotatedWith(Class)
は、引数で指定されたメタアノテーション(@MetaAnnotaion
)で注釈されているアノテーション(@FooAnnotation
)で注釈されている型(Foo
)に絞っている -
Bar
を注釈しているBarAnnotation
はメタアノテーションで注釈されていないので、対象にはならない - Spring Framework の
@Component
とかがメタアノテーションになる
完全修飾名
package example.archunit;
import java.util.List;
public class Hoge {
int primitive;
String object;
int[] primitiveArray;
String[] objectArray;
int[][] nestedPrimitiveArray;
String[][] nestedObjectArray;
List<String> genericClass;
MemberClass memberClass;
StaticMemberClass staticMemberClass;
class MemberClass {}
static class StaticMemberClass {}
}
package example;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.Hoge;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");
final JavaClass hogeClass = javaClasses.get(Hoge.class);
hogeClass.getFields().forEach(field -> {
System.out.println(field.getRawType().getFullName());
});
}
}
int
java.lang.String
[I
[[Ljava.lang.String;
example.archunit.Hoge$StaticMemberClass
[Ljava.lang.String;
[[I
example.archunit.Hoge$MemberClass
java.util.List
-
haveFullyQualifiedName(String)
とかで使う完全修飾名は、 Java 言語仕様の完全修飾名とは一致していない- Java 言語仕様での定義は完全限定名と正規名とバイナリ名を参照
- おそらく、 Class.getName() が返す値と一致している
コードユニットの完全名
package example.archunit;
import java.util.List;
public class Hoge {
Hoge(int i) {}
public void method1() {}
protected String method2(int i, String s, int[] ints, List<String> list) {return null;}
public static class MemberClass {
void method() {}
}
}
package example;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.Hoge;
public class Main {
public static void main(String[] args) {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit");
final JavaClass hogeClass = javaClasses.get(Hoge.class);
hogeClass.getCodeUnits().forEach(method -> System.out.println(method.getFullName()));
final JavaClass memberClass = javaClasses.get(Hoge.MemberClass.class);
memberClass.getCodeUnits().forEach(method -> System.out.println(method.getFullName()));
}
}
example.archunit.Hoge.method1()
example.archunit.Hoge.method2(int, java.lang.String, [I, java.util.List)
example.archunit.Hoge.<init>(int)
example.archunit.Hoge$MemberClass.method()
example.archunit.Hoge$MemberClass.<init>()
- コードユニットの完全名は、
型の完全修飾名.メソッド名(仮引数の型の完全修飾名, ...)
となる- コンストラクタの場合は、
メソッド名
が<init>
となる
- コンストラクタの場合は、
パッケージのパターン指定
-
resideInAPackage(String)
などのパッケージをマッチングさせるメソッドでは、 AspectJ 風の特殊な記法でパッケージのパターンを指定できる - 具体的には、次の2つのパターンが用意されている
-
*
:ドット(.
)以外の、1文字以上の文字列 -
..
:0以上の任意の数のパッケージ
-
- 下図は、パッケージ構成に対してパターンがどのようにマッチするかをまとめている
- 丸印のところが、パターンに対してパッケージ名がマッチするケースになる
制約の定義
-
should()
の後ろで指定する、制約(ルール)を定義するメソッドについて整理する -
that()
と同じで、選択した対象によってshould()
の戻り値の型は次のように変化する
対象の選択 |
should() が返す型 |
---|---|
classes().should() |
ClassesShould |
members().should() |
MembersShould |
codeUnits().should() |
CodeUnitsShould |
constructors().should() |
CodeUnitsShould (※ConstructorsShould は無い) |
methods().should() |
MethodsShould |
fields().should() |
FieldsShould |
制約を定義するメソッドの一覧
注意事項
-
notBe(Class)
のような否定形のメソッドは省略している -
be(Class)
,be(String)
のようにオーバーロードされているメソッドは、1種類だけ(基本的にClass
型のものだけ)記載している
ClassesShould
メソッド | 説明 |
---|---|
accessClassesThat() |
対象の型に、この後で定義する条件を満たす型にアクセスしている箇所が1つ以上あることを制約にする。 否定形での利用が前提。 |
accessField(Class, String) |
対象の型からアクセスしているフィールドのうち、最低1つは指定されたフィールドであることを制約にする。 否定形での利用が前提。 |
be(Class) |
対象の型が、指定されたクラスと一致することを制約にする。 |
beAnnotatedWith(Class) |
対象の型が、指定されたアノテーションで注釈されていることを制約にする。 |
beAnonymousClasses() |
対象の型が匿名クラスであることを制約にする。 |
beAssignableFrom(Class) |
対象の型の変数に、指定された型のインスタンスを代入できることを制約にする。(対象型の変数 = 指定された型のインスタンス; ) |
beAssignableTo(Class) |
対象の型のインスタンスを、指定された型の変数に代入できることを制約にする。(指定された型の変数 = 対象型のインスタンス; ) |
beEnums() |
対象の型が enum であることを制約にする。 |
beInnerClasses() |
対象の型が内部クラスであることを制約にする。 |
beInterfaces() |
対象の型がインタフェースであることを制約にする。 |
beLocalClasses() |
対象の型がローカルクラスであることを制約にする。 |
beMemberClasses() |
対象の型がメンバークラスであることを制約にする。 |
beMetaAnnotatedWith(Class) |
対象の型が、指定されたメタアノテーションで注釈されたアノテーションで注釈されていることを制約にする。 |
beNestedClasses() |
対象の型がネストされた型であることを制約にする。 |
bePackagePrivate() |
対象の型がパッケージプライベートであることを制約にする。 |
bePrivate() |
対象の型が private であることを制約にする。 |
beProtected() |
対象の型が protected であることを制約にする。 |
bePublic() |
対象の型が public であることを制約にする。 |
beTopLevelClasses() |
対象の型がトップレベルであることを制約にする。 |
callConstructor(Class, Class...) |
対象の型で実行しているコンストラクタのうち、最低1つは指定されたコンストラクタと一致することを制約にする。 否定形での利用が前提。 |
callMethod(Class, String, Class...) |
対象の型で実行しているメソッドのうち、最低1つは指定されたメソッドと一致することを制約にする。 否定形での利用が前提。 |
dependOnClassesThat() |
対象の型が、この後で定義する条件を満たす型に依存することを制約にする。 否定形での利用が前提。 |
getField(Class, String) |
対象の型からアクセスしているフィールドのうち、最低1つは指定したフィールドの読み取りであることを制約にする。 否定形での利用が前提。 |
haveFullyQualifiedName(String) |
対象の型の完全修飾名が、指定された文字列と一致することを制約にする。 |
haveModifier(JavaModifier) |
対象の型が、指定された修飾子を持つことを制約にする。 |
haveNameMatching(String) |
対象の型の完全修飾名が、指定された正規表現にマッチすることを制約にする。 |
haveOnlyFinalFields() |
対象の型が持つフィールドが全て final であることを制約にする。 |
haveOnlyPrivateConstructors() |
対象の型が持つコンストラクタが全て private であることを制約にする。 |
haveSimpleName(String) |
対象の型の単純名が、指定された文字列と一致することを制約にする。 |
haveSimpleNameContaining(String) |
対象の型の単純名が、指定された文字列を含むことを制約にする。 |
haveSimpleNameEndingWith(String) |
対象の型の単純名が、指定された文字列で終わることを制約にする。 |
haveSimpleNameStartingWith(String) |
対象の型の単純名が、指定された文字列で始まることを制約にする。 |
implement(Class) |
対象のクラスが、指定されたインタフェースを実装していることを制約にする。 |
onlyAccessClassesThat() |
対象の型が、この後で定義する条件を満たすクラスのインスタンスにだけアクセスしていることを制約にする。 |
onlyAccessFieldsThat(DescribedPredicate) |
対象の型が、指定した条件を満たすフィールドにだけアクセスしていることを制約にする。 |
onlyAccessMembersThat(DescribedPredicate) |
対象の型が、指定した条件を満たすメンバーにだけアクセスしていることを制約にする。 |
onlyBeAccessed() |
対象の型が、このあとに定義する条件を満たすクラスからのみアクセスされていることを制約にする。 |
onlyCallCodeUnitsThat(DescribedPredicate) |
対象の型が、指定した条件を満たすコードユニットだけを呼び出していることを制約にする。 |
onlyCallConstructorsThat(DescribedPredicate) |
対象の型が、指定した条件を満たすコンストラクタだけを呼び出していることを制約にする。 |
onlyCallMethodsThat(DescribedPredicate) |
対象の型が、指定した条件を満たすメソッドだけを呼び出していることを制約にする。 |
onlyDependOnClassesThat() |
対象の型が、この後に定義する条件を満たすクラスにだけ依存していることを制約にする。 |
onlyHaveDependentClassesThat() |
対処の型が、この後に定義する条件を満たすクラスからのみ依存されていることを制約にする。 |
resideInAnyPackage(String...) |
対象の型が、指定したいずれかのパッケージに含まれることを制約にする。 |
resideInAPackage(String) |
対象の型が、指定されたパッケージに含まれることを制約にする。 |
resideOutsideOfPackage(String) |
対象の型が、指定されたパッケージに含まれないこと制約にする。 |
resideOutsideOfPackages(String) |
対象の型が、指定されたいずれのパッケージにも含まれないことを制約にする。 |
setField(Class, String) |
対象の型からアクセスしているフィールドのうち、最低1つは指定したフィールドの書き込みであることを制約にする。 否定形での利用が前提。 |
※DescribedPredicate
を引数に受け取るメソッドは、後述する 事前定義された述語API を使うと比較的簡単に DescribedPredicate
を構築できる。
MembersShould
メソッド | 説明 |
---|---|
beAnnotatedWith(Class) |
対象のメンバーが、指定されたアノテーションで注釈されていることを制約にする。 |
beDeclaredIn(Class) |
対象のメンバーが、指定された型の中で宣言されていることを制約にする。 |
beDeclaredInClassesThat() |
対象のメンバーが、この後に定義する条件を満たす型の中で宣言されていることを制約にする。 |
beMetaAnnotatedWith(Class) |
対象のメンバーが、指定されたメタアノテーションで注釈されたアノテーションで注釈されていることを制約にする。 |
bePackagePrivate() |
対象のメンバーが、パッケージプライベートであることを制約にする。 |
bePrivate() |
対象のメンバーが、 private であることを制約にする。 |
beProtected() |
対象のメンバーが、 protected であることを制約にする。 |
bePublic() |
対象のメンバーが、 public であることを制約にする。 |
haveFullName(String) |
対象のメンバーの完全修飾名が、指定された文字列と一致することを制約にする。 |
haveFullNameMatching(String) |
対象のメンバーの完全修飾名が、指定された正規表現にマッチすることを制約にする。 |
haveModifier(JavaModifier) |
対象のメンバーが、指定された修飾子を持つことを制約にする。 |
haveName(String) |
対象のメンバーの名前が、指定された文字列と一致することを制約にする。 |
haveNameMatching(String) |
対象のメンバーの名前が、指定された正規表現にマッチすることを制約にする。 |
CodeUnitsShould
メソッド | 説明 |
---|---|
declareThrowableOfType(Class) |
対象のコードユニットの throws 句に、指定された例外の型が含まれていることを制約にする。 |
haveRawParameterTypes(Class...) |
対象のコードユニットの仮引数が、指定された型の並びと一致することを制約にする。 |
haveRawReturnType(Class) |
対象のコードユニットの戻り値の型が、指定された型と一致することを制約にする。 |
MethodsShould
メソッド | 説明 |
---|---|
beFinal() |
対象のメソッドが final であることを制約にする。 |
beStatic() |
対象のメソッドが static であることを制約にする。 |
FieldsShould
メソッド | 説明 |
---|---|
beFinal() |
対象のフィールドが final であることを制約にする。 |
beStatic() |
対象のフィールドが static であることを制約にする。 |
haveRawType(Class) |
対象のフィールドの型が指定された型と一致することを制約にする。 |
制約を定義するメソッドの詳細
否定形での利用を前提としたメソッド
- 以下のメソッドは、おそらく否定形で使用することが前提となっている
accessClassesThat()
accessField(Class, String)
getField(Class, String)
setField(Class, String)
callConstructor(Class, Class...)
callMethod(Class, String, Class...)
dependOnClassesThat()
- これらは、いずれも「条件に一致する処理が最低1つあること」を制約にする
- そのままだと、いまいち使い道が分からない
- 制約の内容に対して、出力されるエラーメッセージが関係しておらず、一見すると意味がわからないエラーメッセージになる
- 以下は、
accessClassesThat()
をそのまま利用した場合の例
`-example/archunit/
|-foo/
| `-Foo.java
|-bar/
| `-Bar.java
`-target/
|-NoAccess.java
|-DeclareFooField.java
`-CallFooConstructor.java
package example.archunit.foo;
public class Foo {}
package example.archunit.bar;
public class Bar {}
package example.archunit.target;
public class NoAccess {}
package example.archunit.target;
import example.archunit.foo.Foo;
public class DeclareFooField {
Foo foo;
}
package example.archunit.target;
import example.archunit.bar.Bar;
import example.archunit.foo.Foo;
public class CallFooConstructor {
Foo foo = new Foo();
Bar bar = new Bar();
}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");
ArchRule rule = classes().should().accessClassesThat().resideInAPackage("example.archunit.foo");
rule.check(javaClasses);
}
}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should access classes that reside in a package 'example.archunit.foo'' was violated (2 times):
Constructor <example.archunit.target.DeclareFooField.<init>()> calls constructor <java.lang.Object.<init>()> in (DeclareFooField.java:5)
Constructor <example.archunit.target.NoAccess.<init>()> calls constructor <java.lang.Object.<init>()> in (NoAccess.java:3)
-
accessClassesThat()
は、対象の型が、この後ろで定義した条件を満たす型に1回以上アクセスしていることを制約として定義する - 上記例では、
example.archunit.target
配下のクラスは、1回以上example.archunit.foo
配下のクラスにアクセスしていることが制約として定義されている -
NoAccess
は、全くアクセスしていないのでエラーになっている -
DeclareFooField
は、一見するとexample.archunit.foo.Foo
を使っているので問題ないように見える- しかし、 ArchUnit における「アクセス」は、フィールドを参照したりメソッドを実行していることを表している
- したがって、フィールドを定義しているだけの
DeclareFooField
は「アクセスしていない」ことになり、エラーとなっている
-
CallFooConstructor
は、コンストラクタを呼び出して「アクセスしている」ので、エラーにはなっていない- 関係ないクラス
Bar
にもアクセスしているが、これはこの制約には関係しない
- 関係ないクラス
否定形で使用する
- 上記例を見ると、制約内容に対してエラーメッセージの内容が全然マッチしていなくて、何が原因でエラーになっているのかわからない
- 実際のところ、この
accessClassesThat()
や上で挙げたgetField()
などのメソッドは、否定形で使用されることが想定されている(たぶん)
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");
ArchRule rule = noClasses().should().accessClassesThat().resideInAPackage("example.archunit.foo");
rule.check(javaClasses);
}
}
-
noClasses()
になって、example.archunit.target
配下にはexample.archunit.foo
配下のクラスにアクセスしているクラスは存在しないことが制約となっている
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes should access classes that reside in a package 'example.archunit.foo'' was violated (1 times):
Constructor <example.archunit.target.CallFooConstructor.<init>()> calls constructor <example.archunit.foo.Foo.<init>()> in (CallFooConstructor.java:6)
-
Foo
のコンストラクタを実行しているCallFooConstructor
がエラーになった - エラーメッセージも、制約内容と整合の取れた内容になっていて、問題の原因が理解できるようになっている
only 系のメソッド
- 名前に
only
が付く以下のメソッドは、名前の通り「その対象にだけを利用している・されている」ことを制約にできるonlyAccessClassesThat()
onlyAccessFieldsThat(DescribedPredicate)
onlyAccessMembersThat(DescribedPredicate)
onlyCallCodeUnitsThat(DescribedPredicate)
onlyCallConstructorsThat(DescribedPredicate)
onlyCallMethodsThat(DescribedPredicate)
onlyBeAccessed()
onlyDependOnClassesThat()
onlyHaveDependentClassesThat()
- ただし、この
only
はちょっと癖があるので使い方に注意が必要 - 以下は、
onlyAccessClassesThat()
の利用例
`-example/archunit/
|-foo/
| `-Foo.java
|-bar/
| `-Bar.java
`-target/
|-NoAccess.java
|-AccessFoo.java
`-AccessBar.java
package example.archunit.foo;
public class Foo {
public String value;
}
package example.archunit.bar;
public class Bar {}
package example.archunit.target;
public class NoAccess {}
package example.archunit.target;
import example.archunit.foo.Foo;
public class AccessFoo {
String value = new Foo().value;
}
package example.archunit.target;
import example.archunit.bar.Bar;
public class AccessBar {
Bar bar = new Bar();
}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");
ArchRule rule = classes().should().onlyAccessClassesThat().resideInAPackage("example.archunit.foo");
rule.check(javaClasses);
}
}
-
example.archunit.target
配下のクラスは、example.archunit.foo
パッケージ配下のクラスにだけアクセスしていることを制約にしているつもりのテストコード -
AccessBar
はexample.archunit.bar.Bar
にアクセスしているので、テストは落ちる気がする - 一方で、
NoAccess
はどこにもアクセスしていないしAccessFoo
はFoo
にしかアクセスしていないので、この2つはテストが通りそうな気がする - しかし、実際の結果は次のようになる
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should only access classes that reside in a package 'example.archunit.foo'' was violated (6 times):
Constructor <example.archunit.target.AccessBar.<init>()> calls constructor <example.archunit.bar.Bar.<init>()> in (AccessBar.java:6)
Constructor <example.archunit.target.AccessBar.<init>()> calls constructor <java.lang.Object.<init>()> in (AccessBar.java:5)
Constructor <example.archunit.target.AccessBar.<init>()> sets field <example.archunit.target.AccessBar.bar> in (AccessBar.java:6)
Constructor <example.archunit.target.AccessFoo.<init>()> calls constructor <java.lang.Object.<init>()> in (AccessFoo.java:5)
Constructor <example.archunit.target.AccessFoo.<init>()> sets field <example.archunit.target.AccessFoo.value> in (AccessFoo.java:6)
Constructor <example.archunit.target.NoAccess.<init>()> calls constructor <java.lang.Object.<init>()> in (NoAccess.java:3)
-
AccessBar
がエラーになるのは良いとして、NoAccess
,AccessFoo
もエラーになってしまっている -
NoAccess
は、コンストラクタでObject
クラスのコンストラクタを呼んでいるとしてエラーになっている -
AccessFoo
は、同じくコンストラクタのエラーに加えて、自分自身のフィールドvalue
にアクセスしていることでエラーとなっている - このように、
only
のついた制約はかなり厳しい条件となる - したがって、上記テストを意図通りに動かしたい場合は、アクセスを許可するパッケージを以下のように緩和する必要がある
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPackages("example.archunit.target");
ArchRule rule = classes().should().onlyAccessClassesThat()
.resideInAnyPackage("example.archunit.foo", "example.archunit.target", "java..");
rule.check(javaClasses);
}
}
-
resideInAnyPackage(String...)
を使い、複数のパッケージを指定できるようにしている - 対象のパッケージに、対象自身を含むパッケージと
java
パッケージ配下を追加することで、条件を緩和している
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should only access classes that reside in any package ['example.archunit.foo', 'example.archunit.target', 'java..']' was violated (1 times):
Constructor <example.archunit.target.AccessBar.<init>()> calls constructor <example.archunit.bar.Bar.<init>()> in (AccessBar.java:6)
- これで、当初期待したとおり
AccessBar
だけがエラーになった
対象・条件・制約のカスタマイズ
対象の選択 の表を見ると、「全てのパッケージを対象にする」メソッドは用意されていないことが分かる。
パッケージ以外にも、分析したい視点によって対象は様々なケースが考えられる。
ArchRuleDefinition
では、これら全てのケースに対応したメソッドを用意しておくことはできない。
その代わりに、任意の対象を抽出するための API が用意されている。
package example;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaPackage;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.AbstractClassesTransformer;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ClassesTransformer;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.jupiter.api.Test;
import java.util.stream.Collectors;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses javaClasses = new ClassFileImporter().importPath("build/classes/java/main");
// 全パッケージを対象とする
ClassesTransformer<JavaPackage> packages = new AbstractClassesTransformer<>("packages") {
@Override
public Iterable<JavaPackage> doTransform(JavaClasses classes) {
return classes.stream().map(JavaClass::getPackage).collect(Collectors.toSet());
}
};
// クラスを含むパッケージのみを条件にする
DescribedPredicate<JavaPackage> haveClasses = new DescribedPredicate<>("contains classes") {
@Override
public boolean apply(JavaPackage javaPackage) {
return !javaPackage.getClasses().isEmpty();
}
};
// package-info.java を持つことを制約にする
ArchCondition<JavaPackage> havePackageInfo = new ArchCondition<>("have package-info.java") {
@Override
public void check(JavaPackage javaPackage, ConditionEvents events) {
if (!javaPackage.tryGetPackageInfo().isPresent()) {
events.add(SimpleConditionEvent.violated(javaPackage, javaPackage.getName() + " package does not have package-info.java."));
}
}
};
ArchRule rule = all(packages).that(haveClasses).should(havePackageInfo);
rule.check(javaClasses);
}
}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'packages that contains classes should have package-info.java' was violated (4 times):
example.controller package does not have package-info.java.
example.sandbox package does not have package-info.java.
example.service package does not have package-info.java.
example.target package does not have package-info.java.
対象のカスタマイズ
// 全パッケージを対象とする
ClassesTransformer<JavaPackage> packages = new AbstractClassesTransformer<>("packages") {
@Override
public Iterable<JavaPackage> doTransform(JavaClasses classes) {
return classes.stream().map(JavaClass::getPackage).collect(Collectors.toSet());
}
};
-
all(ClassesTransformer) を使うことで、対象を絞り込む処理をカスタマイズできる
-
ClassesTransformer を実装したクラスを
all()
メソッドに渡す -
AbstractClassesTransformer を継承して作れば、
doTransform(JavaClasses)
だけを実装すればよくなる -
doTransform()
には読み込まれたJavaClasses
が渡されるので、対象にしたいオブジェクトに変換してreturn
する
-
ClassesTransformer を実装したクラスを
-
all()
メソッドを使用した場合、その後のthat()
とshould()
も、同じようにカスタマイズしたクラスを渡す必要がある
条件のカスタマイズ
// クラスを含むパッケージのみを条件にする
DescribedPredicate<JavaPackage> haveClasses = new DescribedPredicate<>("contains classes") {
@Override
public boolean apply(JavaPackage javaPackage) {
return !javaPackage.getClasses().isEmpty();
}
};
-
that(DescribedPredicate)
には、対象を絞り込む条件を実装した DescribedPredicate インスタンスを渡す- コンストラクタ引数には、
ArchRule
のgetDescription()
で返される説明に使用される文言(that
の後ろ)を設定する -
packages that contains classes should have package-info.java
のcontains classes
の部分
- コンストラクタ引数には、
-
apply(T)
メソッドは、受け取った対象が条件に一致する場合にtrue
を返すよう実装する
事前定義された述語 API
-
all()
を使った場合は、常にDescribedPredicate
を実装したクラスを用意しなければならないのかというと、そうでもない - 述語を記述するための事前定義された API が用意されており、それを使えば
all()
を使わない場合と同じような記述ができる
package example;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
public class Main {
public static void main(String[] args) {
DescribedPredicate<JavaClass> resideInTheFooPackage = resideInAPackage("..foo..");
...
}
}
-
JavaClass
のメンバークラスである Predicates に、JavaClass
の条件を絞り込むための述語を記述するための API が用意されている - このように述語を記述するための API を定義したクラスは、以下のものが用意されている
- AccessTarget.Predicates
- CanBeAnnotated.Predicates
- Dependency.Predicates
- HasModifiers.Predicates
- HasName.AndFullName.Predicates
- HasName.Predicates
- HasOwner.Predicates
- HasOwner.Predicates.With
- HasParameterTypes.Predicates
- HasReturnType.Predicates
- HasThrowsClause.Predicates
- HasType.Predicates
- JavaAccess.Predicates
- JavaCall.Predicates
- JavaClass.Predicates
- JavaCodeUnit.Predicates
- JavaFieldAccess.Predicates
- JavaMember.Predicates
条件の連結
package example;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import java.io.Serializable;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
public class Main {
public static void main(String[] args) {
DescribedPredicate<JavaClass> isHogeSerializable = type(Serializable.class).and(name("Hoge"));
...
}
}
-
DescribedPredicate には、条件を連結させるために
and()
やor()
などの様々なメソッドが用意されている - これをつなげることで、より複雑な条件を組み立てられる
連結順序の制約
- 上の例では、
type(Serializable.class).and(name("Hoge"))
のように先にSerializable
型であることを定義して、その後で名前がHoge
である条件を追加している - これを、もし逆順で組み立てようとするとコンパイルエラーになる
DescribedPredicate<HasName> isHogeSerializable = name("Hoge").and(type(Serializable.class));
// java: 不適合な型: com.tngtech.archunit.base.DescribedPredicate<com.tngtech.archunit.core.domain.JavaClass>を
// com.tngtech.archunit.base.DescribedPredicate<? super com.tngtech.archunit.core.domain.properties.HasName>に変換できません:
- 順序を逆にしても意味は変わらないはずだが、 Java の言語仕様の制約上、このコンパイルエラーは回避できない
-
DescribedPredicate<T>
のand()
メソッドの引数は、DescribedPredicate<? super T>
という下限付き境界ワイルドカード型で定義されている - したがって、
-
JavaClass.Predicates.type()
のand()
は、引数の型がDescribedPredicate<? super JavaClass>
となり、
HasName.Predicates.name()
のand()
は、引数の型がDescribedPredicate<? super HasName>
となる
-
-
JavaClass implements HasName
の関係があるので、- 引数
DescribedPredicate<? super JavaClass>
にDescribedPredicate<HasName>
は渡せるが、
引数DescribedPredicate<? super HasName>
にDescribedPredicate<JavaClass>
は渡せない
- 引数
-
- これを回避する方法は、以下の2つのいずれかになる
- コンパイルが通る順序で書く
-
forSubType()
を使って型の情報を変更する
-
forSubType()
を使った場合は、次のような実装になる
package example;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import java.io.Serializable;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
public class Main {
public static void main(String[] args) {
// forSubType() を使って、無理やり型を DescribedPredicate<JavaClass> に変更する
DescribedPredicate<JavaClass> nameIsHoge = name("Hoge").forSubType();
DescribedPredicate<JavaClass> isHogeSerializable = nameIsHoge.and(type(Serializable.class));
...
}
}
制約のカスタマイズ
// package-info.java を持つことを制約にする
ArchCondition<JavaPackage> havePackageInfo = new ArchCondition<>("have package-info.java") {
@Override
public void check(JavaPackage javaPackage, ConditionEvents events) {
if (!javaPackage.tryGetPackageInfo().isPresent()) {
events.add(SimpleConditionEvent.violated(javaPackage, javaPackage.getName() + " package does not have package-info.java."));
}
}
};
-
should(ArchCondition)
には、検証する制約の処理を実装した ArchCondition インスタンスを渡す- コンストラクタ引数には、
ArchRule
のgetDescription()
で返される説明に使用される文言(should
の後ろ)を設定する -
packages that contains classes should have package-info.java
のhave package-info.java
の部分
- コンストラクタ引数には、
-
check(T, ConditionEvents)
メソッドで検証処理を実装する- 検証エラーの場合は、第二引数で受け取った ConditionEvents に違反の情報を追加する
事前定義された API
- こちらも、条件の絞り込みのための述語 API と同じように、事前定義された API が用意されている
- ただし、述語の API はいくつかの
Predicates
クラスに分かれて定義されていたのに対して、制約を定義するための API は ArchConditions クラスに全てまとめられている
package example;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import static com.tngtech.archunit.lang.conditions.ArchConditions.accessClassesThatResideIn;
public class Main {
public static void main(String[] args) {
ArchCondition<JavaClass> resideInFooPackage = accessClassesThatResideIn("..foo..");
...
}
}
参照元の取得
- ArchUnit の Core API は、標準ライブラリのリフレクション API と似た機能を提供している
- しかし、リフレクションにはない独自の重要な機能として、参照元を取得できるという特徴がある
`-example/archunit/
|-foo/
| |-Foo.java
| `-Bar.java
|-fizz/
| |-Fizz.java
| `-Buzz.java
`-target/
`-Hoge.java
package example.archunit.foo;
import example.archunit.target.Hoge;
public class Foo {
void foo() {
new Hoge().method();
}
}
package example.archunit.foo;
import example.archunit.target.Hoge;
public class Bar {
String value = new Hoge().field;
}
package example.archunit.fizz;
public class Fizz {
public void fizz() {}
}
package example.archunit.fizz;
public class Buzz {
public String buzz;
}
package example.archunit.target;
import example.archunit.fizz.Buzz;
import example.archunit.fizz.Fizz;
public class Hoge {
public String field;
public void method() {
new Fizz().fizz();
String value = new Buzz().buzz;
}
}
package example;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.target.Hoge;
public class Main {
public static void main(String[] args) {
final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
final JavaClass hogeClass = classes.get(Hoge.class);
hogeClass.getFieldAccessesToSelf().forEach(System.out::println);
hogeClass.getFieldAccessesFromSelf().forEach(System.out::println);
hogeClass.getMethodCallsToSelf().forEach(System.out::println);
hogeClass.getMethodCallsFromSelf().forEach(System.out::println);
}
}
JavaFieldAccess{origin=JavaConstructor{example.archunit.foo.Bar.<init>()}, target=target{example.archunit.target.Hoge.field}, lineNumber=6, accessType=GET}
JavaFieldAccess{origin=JavaMethod{example.archunit.target.Hoge.method()}, target=target{example.archunit.fizz.Buzz.buzz}, lineNumber=10, accessType=GET}
JavaMethodCall{origin=JavaMethod{example.archunit.foo.Foo.foo()}, target=target{example.archunit.target.Hoge.method()}, lineNumber=7}
JavaMethodCall{origin=JavaMethod{example.archunit.target.Hoge.method()}, target=target{example.archunit.fizz.Fizz.fizz()}, lineNumber=9}
-
getFieldAccessesToSelf()
やgetMethodCallsFromSelf()
などのメソッド、その対象に(から)アクセスしているモノを取得できる -
~ToSelf
は、その対象にアクセスしているモノを抽出し、
~FromSelf
は、その対象からアクセスしているモノを抽出する - ここでは、クラスに(から)アクセスしているモノを全て抽出するメソッドを使用しているが、
hogeClass.getField("field").getAccessesToSelf()
やhogeClass.getMethod("method").getAccessesFromSelf()
のように特定の要素に(から)アクセスしているモノを取得することもできる
参照を表すクラス
- 参照を表すクラスは、上図のような構成になっている
- 参照には向きがあり、
origin
が参照元、target
が参照先を表している
参照先を表すクラス
- 参照先を表すクラスは、次の3つが存在する
FieldAccessTarget
MethodCallTarget
ConstructorCallTarget
- これらは、参照を表すクラス(
JavaFieldAccess
など)と1対1の関係になっている - 一方で、具体的な参照先を表すクラス(
JavaField
など)との関係は、0
や*
があったりするのが分かる- アクセスしているはずなのに具体的な参照先が
0
であったり、メソッドの呼び出し先が複数あるのは、普通に考えると不自然に思える
- アクセスしているはずなのに具体的な参照先が
アクセス先が0になるケース
`-src/
|-main/java/
| `-example/archunit/
| |-foo/
| | `-Parent.java
| `-target/
| |-Child.java
| `-Hoge.java
`-test/resources/
`-archunit.properties
resolveMissingDependenciesFromClassPath=false
package example.archunit.foo;
public class Parent {
public String field;
public void method() {}
}
package example.archunit.target;
import example.archunit.foo.Parent;
public class Child extends Parent {}
package example.archunit.target;
public class Hoge {
void hoge() {
final Child child = new Child();
child.field = "test";
child.method();
}
}
package example;
import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.domain.JavaFieldAccess;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.target.Hoge;
import java.util.Set;
public class Main {
public static void main(String[] args) {
final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit.target");
final JavaClass hogeClass = classes.get(Hoge.class);
for (JavaFieldAccess javaFieldAccess : hogeClass.getFieldAccessesFromSelf()) {
final AccessTarget.FieldAccessTarget target = javaFieldAccess.getTarget();
final JavaField javaField = target.resolveField().orNull();
System.out.println("owner=" + target.getOwner() + ", javaField=" + javaField);
}
for (JavaMethodCall javaMethodCall : hogeClass.getMethodCallsFromSelf()) {
final AccessTarget.MethodCallTarget target = javaMethodCall.getTarget();
final Set<JavaMethod> javaMethods = target.resolve();
System.out.println("owner=" + target.getOwner() + ", javaMethods=" + javaMethods);
}
}
}
-
example.archunit.target
をインポートして、Hoge
からアクセスしている先を抽出している
owner=JavaClass{name='example.archunit.target.Child'}, javaField=null
owner=JavaClass{name='example.archunit.target.Child'}, javaMethods=[]
- 具体的な参照先の情報は空になっている
- このように、参照先のフィールドやメソッドが親クラスに定義されていて、その親クラスがインポート対象に含まれていない場合に、具体的な参照先が空になる
- ちなみに、自動的な読み込みをオンにして実行したら、下のように情報がちゃんと取れる
owner=JavaClass{name='example.archunit.target.Child'}, javaField=JavaField{example.archunit.foo.Parent.field}
owner=JavaClass{name='example.archunit.target.Child'}, javaMethods=[JavaMethod{example.archunit.foo.Parent.method()}]
メソッドのアクセス先が複数になるケース
`-example/archunit/
|-foo/
| |-FooInterface.java
| |-BarInterface.java
| |-AbstractClass.java
| `-ConcreteClass.java
`-target/
`-Hoge.java
package example.archunit.foo;
public interface FooInterface {
void method();
}
package example.archunit.foo;
public interface BarInterface {
void method();
}
- 全く同じシグネチャのメソッドが、
FooInterface
とBarInterface
の両方に定義されている
package example.archunit.foo;
public abstract class AbstractClass implements FooInterface, BarInterface {}
-
FooInterface
とBarInterface
の両方を実装した抽象クラスがあるが、インタフェースで定義されている抽象メソッドを実装していない
package example.archunit.foo;
public class ConcreteClass extends AbstractClass {
@Override
public void method() {}
}
- 具象クラスでは、当然抽象メソッドを実装している
package example.archunit.target;
import example.archunit.foo.AbstractClass;
import example.archunit.foo.ConcreteClass;
public class Hoge {
void hoge() {
AbstractClass cc = new ConcreteClass();
cc.method();
}
}
- 抽象メソッド(
method()
) を、抽象クラス(AbstractClass
)をレシーバにして実行している
package example;
import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import example.archunit.target.Hoge;
import java.util.Set;
public class Main {
public static void main(String[] args) {
final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit.target");
final JavaClass hogeClass = classes.get(Hoge.class);
for (JavaMethodCall javaMethodCall : hogeClass.getMethodCallsFromSelf()) {
final AccessTarget.MethodCallTarget target = javaMethodCall.getTarget();
final Set<JavaMethod> javaMethods = target.resolve();
System.out.println("owner=" + target.getOwner() + ", javaMethods=" + javaMethods);
}
}
}
- フィールドのときと同じように、呼び出し先のメソッドの情報を出力している
owner=JavaClass{name='example.archunit.foo.AbstractClass'}, javaMethods=[JavaMethod{example.archunit.foo.BarInterface.method()}, JavaMethod{example.archunit.foo.FooInterface.method()}]
- 解決されたメソッドが、インタフェースで定義された2つのメソッドになっている
- 要するに、下図のような関係になっていると、 ArchUnit は呼び出し先の具体的なメソッドを一意に特定できなくなり、候補となるメソッドが全て取得されるようになっている
- ちなみに、
AbstractClass
でmethod()
を実装していた場合は以下のようになる
owner=JavaClass{name='example.archunit.foo.AbstractClass'}, javaMethods=[JavaMethod{example.archunit.foo.AbstractClass.method()}]
- ところで、
AbstractClass
が抽象メソッドを実装していない状態(ConcreteClass
が実装している最初の状態)で、メソッド呼び出しのレシーバをConcreteClass
にすると、次のような結果になる
package example.archunit.target;
import example.archunit.foo.ConcreteClass;
public class Hoge {
void hoge() {
ConcreteClass cc = new ConcreteClass();
cc.method();
}
}
owner=JavaClass{name='example.archunit.foo.ConcreteClass'}, javaMethods=[JavaMethod{example.archunit.foo.ConcreteClass.method()}, JavaMethod{example.archunit.foo.BarInterface.method()}, JavaMethod{example.archunit.foo.FooInterface.method()}]
-
ConcreteClass.method()
だけになるのかなと思ったら、インタフェースも抽出された - この動きの理由はよくわからない
エラーの説明を変更する
`-example/archunit/
|-foo/
| `-Foo.java
`-bar/
`-Bar.java
package example.archunit.foo;
import example.archunit.bar.Bar;
public class Foo {
Bar bar;
}
package example.archunit.bar;
public class Bar {}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
// foo パッケージは bar パッケージに依存してはいけない
ArchRule rule = noClasses().that().resideInAPackage("..foo..")
.should().dependOnClassesThat().resideInAPackage("..bar..");
rule.check(classes);
}
}
Rule 'no classes that reside in a package '..foo..' should depend on classes that reside in a package '..bar..'' was violated
- デフォルトでは、
ArchRule
の内容に従って自動的に組み立てられた説明が出力される - この説明はかなり分かりやすい形になっているので、たいていの場合はデフォルトの出力で問題はない
- しかし、次のようなケースではデフォルトの説明だけでは十分ではないことがありえる
- ルールの意図を説明したい場合(何故そのようなルールを設けているのか、設計の意図を伝えたい)
-
or
やand
で条件を組み合わせた結果、デフォルトで出力される説明が分かりにくくなった場合
- エラー時の説明は、以下の方法で調整できる
ルールの意図を追加する
package example;
...
public class ArchUnitTest {
@Test
void test() {
final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = noClasses().that().resideInAPackage("..foo..")
.should().dependOnClassesThat().resideInAPackage("..bar..")
.because("俺が気に入らないから");
rule.check(classes);
}
}
Rule 'no classes that reside in a package '..foo..' should depend on classes that reside in a package '..bar..', because 俺が気に入らないから' was violated
-
ArchRule
の定義の中でbecause(String)
メソッドを使うことで、説明の後ろに意図を説明する because 文が追加される
説明を完全に置き換える
package example;
...
public class ArchUnitTest {
@Test
void test() {
final JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = noClasses().that().resideInAPackage("..foo..")
.should().dependOnClassesThat().resideInAPackage("..bar..")
.as("foo が bar に依存するとかマジないわー");
rule.check(classes);
}
}
Rule 'foo が bar に依存するとかマジないわー' was violated
-
as(String)
メソッドを使用すると、エラー時の説明を完全に置き換えることが可能 -
because(String)
との併用も可能
違反を無視する
- 既存プロジェクトに ArchUnit を適用していて段階的にチェックを入れていきたいような場合、一部のコードは制約違反があってもテストが失敗しないようにしたくなる
- 一部のコードだけテストの対象外にしたい場合、例えば
that()
の後で対象となるコードを絞り込むといった方法が考えられる - また、これ以外にも ArchUnit には特定の制約違反があっても無視するようにできる手段が用意されている
`-src/
|-main/java/
| `-example/archunit/
| |-foo/
| | `-Foo.java
| `-target/
| |-Hoge.java
| `-Fuga.java
`-test/resources/
`-archunit_ignore_patterns.txt
- クラスパスのルートに
archunit_ignore_patterns.txt
というテキストファイルが来るようにしている
# Hoge は特別扱い
.*Hoge.*
package example.archunit.foo;
public class Foo {}
package example.archunit.target;
import example.archunit.foo.Foo;
public class Hoge {
Foo foo;
}
package example.archunit.target;
import example.archunit.foo.Foo;
public class Fuga {
Foo foo;
}
- いずれも、
Foo
クラスに依存している
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = noClasses().that().resideInAPackage("..target..")
.should().dependOnClassesThat().resideInAPackage("..foo..");
rule.check(classes);
}
}
-
target
パッケージ以下のクラスはfoo
パッケージ以下のクラスに依存しないことをテスト - 通常なら、
Hoge
,Fuga
両方のクラスがエラーになる
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..target..' should depend on classes that reside in a package '..foo..'' was violated (1 times):
Field <example.archunit.target.Fuga.foo> has type <example.archunit.foo.Foo> in (Fuga.java:0)
- 実際にエラーになったのは、
Fuga
クラスのみになった - このように、クラスパスルートに
archunit_ignore_patterns.txt
というテキストファイルを配置することで、特定の制約違反を無視できるようになる -
archunit_ignore_patterns.txt
には、無視したい制約違反のメッセージとマッチさせる正規表現を記述する- クラス名とかではなく、違反のメッセージにマッチさせる点に注意
- 上記エラーメッセージでいうと、
Field <example.archunit.target.Fuga.foo> has type <example.archunit.foo.Foo> in (Fuga.java:0)
の部分にマッチさせる正規表現となる
-
#
で始まる行はコメント扱いになる
Library API
レイヤードアーキテクチャのテスト
仮に上図のように定義された依存関係をテストする場合の例。
`-example/archunit/
|-domain/
| |-RepositoryInterface.java
| `-DomainObject.java
|-infrastructure/
| `-RepositoryImpleClass.java
|-service/
| `-ServiceClass.java
`-presentation/
`-PresentationClass.java
package example.archunit.domain;
public class DomainClass {
public String method() {
return "domain";
}
}
package example.archunit.domain;
public interface RepositoryInterface {
DomainClass find();
}
package example.archunit.infrastructure;
import example.archunit.domain.DomainClass;
import example.archunit.domain.RepositoryInterface;
public class RepositoryImplClass implements RepositoryInterface {
@Override
public DomainClass find() {
return new DomainClass();
}
}
package example.archunit.service;
import example.archunit.domain.DomainClass;
import example.archunit.domain.RepositoryInterface;
import example.archunit.presentation.PresentationClass;
public class ServiceClass {
// ★Service が Presentation に依存してしまっている
private final PresentationClass presentationClass;
private final RepositoryInterface repositry;
public ServiceClass(PresentationClass presentationClass, RepositoryInterface repositry) {
this.presentationClass = presentationClass;
this.repositry = repositry;
}
public String method() {
DomainClass domainObject = repositry.find();
return domainObject.method();
}
}
package example.archunit.presentation;
import example.archunit.service.ServiceClass;
public class PresentationClass {
private final ServiceClass service;
public PresentationClass(ServiceClass service) {
this.service = service;
}
public String method() {
return service.method();
}
}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.library.Architectures.*;
public class ArchUnitTest {
@Test
void test() {
JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = layeredArchitecture()
.layer("Domain").definedBy("example.archunit.domain..")
.layer("Infrastructure").definedBy("example.archunit.infrastructure..")
.layer("Service").definedBy("example.archunit.service..")
.layer("Presentation").definedBy("example.archunit.presentation..")
.whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("Infrastructure").mayOnlyBeAccessedByLayers("Service", "Presentation")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Presentation", "Service", "Infrastructure");
rule.check(classes);
}
}
- Architectures.layeredArchitecture() を使って、レイヤー間の依存関係を定義している
-
layer(String).definedBy(String)
で、レイヤーの名前と含まれるパッケージを定義している -
whereLayer(String)
とmayNotBeAccessedByAnyLayer()
とmayOnlyBeAccessedByLaysers(String...)
で、レイヤー間の依存関係を定義している-
mayNotBeAccessedByAnyLayer()
は、そこからも依存されないことを定義している -
mayOnlyBeAccessedByLayers(String...)
は、whereLayer(String)
で指定したレイヤーにアクセスできるレイヤー(複数可)を定義している
-
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'Layered architecture consisting of
layer 'Domain' ('example.archunit.domain..')
layer 'Infrastructure' ('example.archunit.infrastructure..')
layer 'Service' ('example.archunit.service..')
layer 'Presentation' ('example.archunit.presentation..')
where layer 'Presentation' may not be accessed by any layer
where layer 'Service' may only be accessed by layers ['Presentation']
where layer 'Infrastructure' may only be accessed by layers ['Service', 'Presentation']
where layer 'Domain' may only be accessed by layers ['Presentation', 'Service', 'Infrastructure']' was violated (2 times):
Constructor <example.archunit.service.ServiceClass.<init>(example.archunit.presentation.PresentationClass, example.archunit.domain.RepositoryInterface)> has parameter of type <example.archunit.presentation.PresentationClass> in (ServiceClass.java:0)
Field <example.archunit.service.ServiceClass.presentationClass> has type <example.archunit.presentation.PresentationClass> in (ServiceClass.java:0)
-
Service
レイヤーがPresentation
レイヤーに依存してしまっているので、テストはエラーとなった
循環参照のチェック
`-example/archunit/
|-one/
| |-One.java
| `-Foo.java
|-two/
| `-Two.java
|-three/
| `-Three.java
|
`-sub/
|-hoge/
| `-Hoge.java
|-fuga/
| `-Fuga.java
`-piyo/
`-Piyo.java
package example.archunit.one;
import example.archunit.two.Two;
public class One {
private Two two;
public One(Two two) {
this.two = two;
}
}
package example.archunit.two;
import example.archunit.three.Three;
public class Two {
private Three three;
public Two(Three three) {
this.three = three;
}
}
package example.archunit.three;
import example.archunit.one.Foo;
public class Three {
private Foo foo;
public Three(Foo foo) {
this.foo = foo;
}
}
-
one.One
->two.Two
->three.Three
->one.Foo
という形で、パッケージの循環参照が発生している - 実装は省略するが、
sub
以下のクラスはsub.hoge.Hoge
->sub.fuga.Fuga
->sub.piyo.Piyo
->sub.hoge.Hoge
-> ... のような循環参照になっている
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.*;
public class ArchUnitTest {
@Test
void test1() {
JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = slices().matching("example.archunit.(*)").should().beFreeOfCycles();
rule.check(classes);
}
@Test
void test2() {
JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
ArchRule rule = slices().matching("example.archunit.(**)").should().beFreeOfCycles();
rule.check(classes);
}
}
-
test1()
は、matching()
の引数が"example.archunit.(*)"
、
test2()
は、matching()
の引数が"example.archunit.(**)"
になっている
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'slices matching 'example.archunit.(*)' should be free of cycles' was violated (1 times):
Cycle detected: Slice one -> Slice two -> Slice three -> Slice one
...
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'slices matching 'example.archunit.(**)' should be free of cycles' was violated (2 times):
Cycle detected: Slice one -> Slice two -> Slice three -> Slice one
...
Cycle detected: Slice sub.fuga -> Slice sub.piyo -> Slice sub.hoge -> Slice sub.fuga
...
- パッケージの循環参照をチェックするには、まず SlicesRuleDefinition.slices() と matching(String) で対象のパッケージを絞り込む
-
matching(String)
の引数には、対象のパッケージを特定するためのパターンを指定する - 基本は resideInAPackage(String) で指定していたパターン と同じ
- ただし、対象のパッケージを限定するための丸括弧
()
が必要- 丸括弧で囲われた部分が異なるパッケージ間での依存の有無がチェックされる
-
a.(*)..
のような指定の場合-
a.b
とa.c
の間で循環参照があればエラーになる -
a.b.d
とa.b.e
の間で循環参照があってもエラーにはならない
-
-
(*)
は、ドットを含まない任意の文字列にマッチし、(**)
はドットを含んだ任意の文字列とマッチする - したがって、
a.(*)
はa
直下のパッケージにマッチし、a.(**)
は孫も含むa
の下の全てのパッケージにマッチする - 詳しくは、 PackageMatcher の Javadoc を参照
-
-
beFreeOfCycles() で、循環参照が無いことを制約として定義している
-
be free of chcles
は、「サイクル(循環参照)が無い」と翻訳するらしい1(サイクルが自由=サイクルして良い、かと思って最初混乱した)
-
PlantUML で描いた図を使ってチェックする
`-src/
|-main/java/
| `-example/archunit/
| |-util/
| | `-UtilClass.java
| |-order/
| | `-OrderClass.java
| |-shipment/
| | `-ShipmentClass.java
| `-sales/
| `-SalesClass.java
`-test/resources/
`-component-diagram.pu
-
component-diagram.pu
という PlantUML のファイルが追加されている
@startuml ArchUnit
[受注] <<..order..>> as order
[出荷] <<..shipment..>> as shipment
[売上] <<..sales..>> as sales
order <-- shipment
shipment <-- sales
@enduml
- PlantUML の コンポーネント図 を使って、パッケージ間の依存関係を記述している
- ステレオタイプで、マッチさせるパッケージのパターンを設定しておく
package example.archunit.order;
import example.archunit.util.UtilClass;
public class OrderClass {
UtilClass utilClass;
}
package example.archunit.shipment;
import example.archunit.order.OrderClass;
public class ShipmentClass {
OrderClass orderClass;
}
package example.archunit.sales;
import example.archunit.order.OrderClass;
import example.archunit.shipment.ShipmentClass;
public class SalesClass {
ShipmentClass shipmentClass;
OrderClass orderClass; // order パッケージに依存している
}
package example;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.plantuml.PlantUmlArchCondition;
import org.junit.jupiter.api.Test;
import java.net.URL;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configurations.*;
import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.*;
public class ArchUnitTest {
@Test
void test1() {
JavaClasses classes = new ClassFileImporter().importPackages("example.archunit");
URL diagram = getClass().getResource("/component-diagram.pu");
PlantUmlArchCondition condition = adhereToPlantUmlDiagram(diagram, consideringOnlyDependenciesInDiagram());
ArchRule rule = classes().should(condition);
rule.check(classes);
}
}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes should adhere to PlantUML diagram <component-diagram.pu> while ignoring dependencies not contained in the diagram' was violated (1 times):
Field <example.archunit.sales.SalesClass.orderClass> has type <example.archunit.order.OrderClass> in (SalesClass.java:0)
-
PlantUmlArchCondition.adhereToPlantUmlDiagram(...) メソッドを使用して PlantUML のテキストを読み込むことで、 PlantUML に記述した内容と実装を比較検証する
PlantUmlArchCondition
インスタンスを生成できる-
adhereToPlantUmlDiagram()
の第一引数には、読み込む PlantUML のファイルを渡す(File
,URL
,Path
など) - 第二引数には、検証の範囲を絞り込む設定(PlantUmlArchCondition.Configuration)を渡す
-
PlantUmlArchCondition.Configuration
に、以下3つのファクトリメソッドが用意されているので、そのいずれかを使用する-
consideringAllDependencies()
- 全てのクラス(
java.lang.Object
も含む)への依存を対象にして検証する - めっちゃシビアだけど、コンポーネントの描き忘れで検証を見落とすリスクは減る
- 全てのクラス(
-
consideringOnlyDependenciesInDiagram()
- PlantUML の中に登場するコンポーネントへの依存を対象にして検証する
- 前述の例では、
OrderClass
はUtilClass
に依存していたが、 PlantUML の図中に含まれていないので検証では特にエラーにもなっていなかった - コンポーネント図への記述を忘れると検証されない(見逃す恐れがある)、ということなので注意
-
consideringOnlyDependenciesInAnyPackage(String, String...)
- PlantUML の中に登場し、かつ引数で指定したパターンにマッチするパッケージを対象にして検証する
-
-
JUnit5 サポート
-
ClassFileImporter
によるクラスの読み込みは、プロジェクトの規模によっては時間がかかるようになり、サイズも大きくなる恐れがある - ArchUnit には、一度読み込んだクラスの情報をキャッシュし、各テスト間で簡単に使いまわしできるようにする JUnit の拡張機能が用意されている
- JUnit4 もサポートされているが、ここでは JUnit5 で検証する
Hello World
`-example/archunit/
|-foo/
| `-Foo.java
`-bar/
`-Bar.java
package example.archunit.foo;
import example.archunit.bar.Bar;
public class Foo {
Bar bar;
}
package example.archunit.bar;
public class Bar {}
package example;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
@AnalyzeClasses(packages = "example.archunit")
public class ArchUnitTest {
@ArchTest
static ArchRule fooDoesNotDependOnBar = noClasses().that().resideInAPackage("..foo..")
.should().dependOnClassesThat().resideInAPackage("..bar..");
}
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..foo..' should depend on classes that reside in a package '..bar..'' was violated (1 times):
Field <example.archunit.foo.Foo.bar> has type <example.archunit.bar.Bar> in (Foo.java:0)
説明
...
import com.tngtech.archunit.junit.AnalyzeClasses;
...
@AnalyzeClasses(packages = "example.archunit")
public class ArchUnitTest {
...
- まずは、テストクラスを @AnalyzeClasses で注釈する
- このアノテーションで、読み込む範囲を定義する
- このアノテーションは、
ClassFileImporter
の機能と対応している - ここでは
packages
で読み込むパッケージを指定している
@ArchTest
static ArchRule fooDoesNotDependOnBar = noClasses().that().resideInAPackage("..foo..")
.should().dependOnClassesThat().resideInAPackage("..bar..");
- 次に、通常と同じ用に
ArchRuleDefinition
を使って検証したい制約(ArchRule
)を定義する - 通常と異なるのは、作成した
ArchRule
を static フィールドに保存して、@ArchTest で注釈している点- これにより、
@AnalyzeClasses
で読み込まれたクラスに対してArchRule
のcheck()
が自動的に実行されるようになる - static でなくても動くけど、テストケースごとにインスタンスを生成させる意味もないので static の方がスマートだと思う
- 可視性は
public
でもprivate
でも動いたので、何もつけなくて良さげ
- これにより、
テストクラスを読み込まないようにする
-
ClassFileImporter
のときは、withImportOption()
にImportOption.Predefined.DO_NOT_INCLUDE_TESTS
を渡すことでテストクラスを読み込み対象から外すことができた -
@AnalyzeClasses
にも、同じようにImportOption
を指定する方法が用意されていて、以下のように設定する
package example;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
...
@AnalyzeClasses(packages = "example.archunit", importOptions = ImportOption.DoNotIncludeTests.class)
public class ArchUnitTest {
...
}
-
importOptions
に、ImportOption
を実装したクラスのClass
オブジェクトを指定する -
ImportOption
には、Predefined
の定数と同じようにImporOption
を実装したクラスがあらかじめメンバークラスとして定義されているので、それらを指定できる
その他のクラスの読み込み方
-
packages
のパッケージ指定以外にも、以下のような指定方法が用意されている -
packagesOf
による、Class
指定の読み込み@AnalyzeClasses(packagesOf = {Foo.class, Bar.class})
- この場合、指定された
Class
が存在するパケージが読み込みの対象となる - この方法には、リファクタリングでパッケージ構成などが変わった場合に記述を修正しなくて済むというメリットがある(IDE でのリファクタリングが前提)
-
LocationProvider による実装
@AnalyzeClasses(locations = MyLocationProvider.class)
-
locations
に、LocationProvider
を実装したクラスのClass
オブジェクトを渡す -
LocationProvider
のget(Class)
メソッドでは、読み込み対象となる場所を Location のSet
で返すように実装する
キャッシュの制御
-
@AnalyzeClasses
によって読み込まれたクラスはキャッシュされ、同じテストクラス内の各テストで再利用される - また、
@AnalyzeClasses
によって読み込まれたクラスは、デフォルトではそのテストクラスが終了したあともキャッシュされる- そして、別のテストクラスで読み込み場所の指定が同じ
@AnalyzeClasses
があったときは、キャッシュされた情報が再利用される - このキャッシュは、ヒープが足りなくなってきたときに破棄されるようになっている
- そして、別のテストクラスで読み込み場所の指定が同じ
- 読み込む対象が同じで使い回す方が効率がいい場合は、このデフォルトの動きで問題ない
- しかし、あるテストクラスでしか指定しない特別な読み込み場所だったりした場合は、他のテストクラスで再利用されることは無いので、キャッシュはヒープを圧迫するだけでむしろデメリットとなり得る
- そこで、次のように
@AnalyzeClasses
を設定することでキャッシュの生存期間を絞ることができるようになっている
package example;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.CacheMode;
...
@AnalyzeClasses(packages = "example.archunit", cacheMode = CacheMode.PER_CLASS)
public class ArchUnitTest {
...
}
-
cacheMode
に CacheMode.PER_CLASS を指定することで、キャシュの生存期間をそのテストクラスの実行中だけに絞ることができる
ルールのグループ化
package example;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class FooRules {
@ArchTest
static ArchRule fooDoesNotDependOnBar = noClasses().that().resideInAPackage("..foo..")
.should().dependOnClassesThat().resideInAPackage("..bar..");
}
package example;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
public class BarRules {
@ArchTest
static ArchRule classesInBarAreNotDeprecated = classes().that().resideInAnyPackage("..bar..")
.should().notBeAnnotatedWith(Deprecated.class);
}
- クラスを
@AnalyzeClasses
で注釈せず、@ArchTest
で注釈したルールだけを宣言したFooRules
とBarRules
を用意する
package example;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchRules;
import com.tngtech.archunit.junit.ArchTest;
@AnalyzeClasses(packages = "example.archunit")
public class ArchUnitTest {
@ArchTest
static ArchRules fooRules = ArchRules.in(FooRules.class);
@ArchTest
static ArchRules barRules = ArchRules.in(BarRules.class);
}
-
ArchRules.in(Class)
を使ってルールだけを宣言したクラス(FooRules
,BarRules
)を読み込み、ArchRules
型のフィールドに格納し、@ArchTest
で注釈する - これを実行すれば、
FooRules
,BarRules
内の全ての@ArchTest
で注釈されたルールがテストされる - このように、ルールをグループ化してまとめることができる
- 例えば、
ServiceLayerRules
,DomainLayerRules
みたいな感じでまとめることができる
- 例えば、
- 汎用的なルールであれば、様々なプロジェクトで使い回せるようにまとめることもできるかもしれない