これは、なにをしたくて書いたもの?
ArchUnitというものを試してみたいな、ということで。
ArchUnit
ArchUnitは、Javaソースコードのアーキテクチャーをチェックするためのユニットテストフレームワークです。
ArchUnit is a free, simple and extensible library for checking the architecture of your Java code using any plain Java unit test framework.
Unit test your Java architecture - ArchUnit
ArchUnitを使うことで、パッケージやクラス、レイヤーやスライスの依存関係、循環参照などをチェックできます。
That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more.
Why test your architecture? - ArchUnit
Getting Startedやドキュメントはこちら。
archunit 1.0.1 javadoc (com.tngtech.archunit)
ユースケースのページを見ると、イメージしやすいですね。
サンプルはこちら。
https://github.com/TNG/ArchUnit-Examples
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.7 2023-04-18 OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2) OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, mixed mode, sharing) $ mvn --version Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.7, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-72-generic", arch: "amd64", family: "unix"
準備
Maven依存関係など。今回は、チェックの都合上、Jakarta EEのWeb Profileを使うことにします。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> <scope>test</scope> </dependency> </dependencies>
使用するテスティングフレームワークは、JUnit 5とします。
ArchUnitを依存関係に加える
ArchUnitを依存関係に加えましょう。JUnit 5向けの場合はこちらになります。
<dependency> <groupId>com.tngtech.archunit</groupId> <artifactId>archunit-junit5</artifactId> <version>1.0.1</version> <scope>test</scope> </dependency>
JUnit 4向けの場合はこちら。
基本的な使い方
基本的な使い方は、Getting Started(User Guide含む)を見るのがよいのではないかと思います。
Getting Startedページのサンプルがだいぶ物悲しいのですが、こちらが基本になります。
@Test public void some_architecture_rule() { JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp"); ArchRule rule = classes()... // see next section rule.check(importedClasses); }
ステップとしては、「クラスをインポートする」、
JavaClasses importedClasses = new ClassFileImporter().importPackages("com.myapp");
「アーキテクチャーのルールを定義する」、
ArchRule rule = classes()... // see next section
「インポートしたクラスに対して、定義したルールを使ってチェックする」となります。
rule.check(importedClasses);
ユーザーガイドでは、こちら。
こちらは、「com.mycompany.myapp
パッケージ内を対象に、service
パッケージ内のクラスはcontroller
または
service
パッケージからのみアクセス可能」というチェックをしています。
@Test public void Services_should_only_be_accessed_by_Controllers() { JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp"); ArchRule myRule = classes() .that().resideInAPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..controller..", "..service.."); myRule.check(importedClasses); }
このあたりが読めないと、なにをやっているのかよくわからなくなります…。
チェック対象
とりあえず、ArchUnitでのチェック対象がないと始まりません。
こんな感じのソースコードを用意しました。
$ tree src/main/java/org/littlewings/archunit src/main/java/org/littlewings/archunit ├── JaxrsActivator.java ├── bar │ ├── repository │ │ └── BarRepository.java │ ├── resource │ │ └── BarResource.java │ └── service │ └── BarService.java └── foo ├── repository │ └── FooRepository.java ├── resource │ └── FooResource.java └── service └── FooService.java 8 directories, 7 files
JAX-RSの有効化。
src/main/java/org/littlewings/archunit/JaxrsActivator.java
package org.littlewings.archunit; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
JAX-RSリソースクラス。
src/main/java/org/littlewings/archunit/foo/resource/FooResource.java
package org.littlewings.archunit.foo.resource; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import org.littlewings.archunit.foo.service.FooService; @Path("foo") @ApplicationScoped public class FooResource { @Inject FooService fooService; @GET @Produces(MediaType.TEXT_PLAIN) public String foo() { return fooService.message(); } }
Serviceクラス。
src/main/java/org/littlewings/archunit/foo/service/FooService.java
package org.littlewings.archunit.foo.service; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import org.littlewings.archunit.foo.repository.FooRepository; @Transactional @ApplicationScoped public class FooService { @Inject FooRepository fooRepository; public String message() { return fooRepository.get(); } public String messagePlain() { return "Hello Foo!!"; } }
Repositoryクラス。
src/main/java/org/littlewings/archunit/foo/repository/FooRepository.java
package org.littlewings.archunit.foo.repository; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class FooRepository { public String get() { return "Hello Foo!!"; } }
あくまでArchUnitでのテスト用のクラスなので、中身はあまり気にせず…。
barという方のパッケージは、クラス名などが変わっただけなので割愛します。同じ構成のパッケージを複数用意するためだけのものです。
はじめてのArchUnit
では、ArchUnitを使ってみましょう。最初に作成したテストコードはこちら。
src/test/java/org/littlewings/archunit/ArchUnitHelloWorldTest.java
package org.littlewings.archunit; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; import org.junit.jupiter.api.Test; public class ArchUnitHelloWorldTest { @Test public void gettingStarted1() { JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit"); // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可 ArchRule rule = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service.."); rule.check(classes); } @Test public void gettingStarted2() { JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit"); // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみ呼び出されていること ArchRule rule = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..resource..", "..service.."); rule.check(classes); } }
どちらも、serviceパッケージ内のクラスがresourceまたはserviceパッケージからのみ呼び出されていることを確認するテストです。
厳密には少し差異がありますが。
クラスをインポートして
JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");
ルールを定義。
// serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可 ArchRule rule = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");
チェック。
rule.check(classes);
このテストはOKになります。
ここで、FooRepository
およびBarRepository
からServiceクラスにアクセスしてみます。
@ApplicationScoped public class FooRepository { @Inject FooService fooService; public String get() { //return "Hello Foo!!"; return fooService.messagePlain(); } } @ApplicationScoped public class BarRepository { @Inject BarService barService; public String get() { //return "Hello Bar!!"; return barService.messagePlain(); } }
すると、2つのテストはそれぞれ失敗します。
## gettingStarted1 java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..service..' should only have dependent classes that reside in any package ['..resource..', '..service..']' was violated (4 times): Field <org.littlewings.archunit.bar.repository.BarRepository.barService> has type <org.littlewings.archunit.bar.service.BarService> in (BarRepository.java:0) Field <org.littlewings.archunit.foo.repository.FooRepository.fooService> has type <org.littlewings.archunit.foo.service.FooService> in (FooRepository.java:0) Method <org.littlewings.archunit.bar.repository.BarRepository.get()> calls method <org.littlewings.archunit.bar.service.BarService.messagePlain()> in (BarRepository.java:14) Method <org.littlewings.archunit.foo.repository.FooRepository.get()> calls method <org.littlewings.archunit.foo.service.FooService.messagePlain()> in (FooRepository.java:14) ## gettingStarted2 java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in any package ['..service..'] should only be accessed by any package ['..resource..', '..service..']' was violated (2 times): Method <org.littlewings.archunit.bar.repository.BarRepository.get()> calls method <org.littlewings.archunit.bar.service.BarService.messagePlain()> in (BarRepository.java:14) Method <org.littlewings.archunit.foo.repository.FooRepository.get()> calls method <org.littlewings.archunit.foo.service.FooService.messagePlain()> in (FooRepository.java:14)
gettingStarted1の方は依存関係を持った時点でNGとなるので、フィールド定義が違反になっています。
メソッド呼び出しの方は、両方のテストケースでNGになっていますね。
OKそうです。
3つのAPI
ArchUnitには、Core API、Lang API、Library APIの3つがあります。
Core API
Core APIはリフレクションに似たAPIで、クラスやフィールド、メソッドなどを扱います。主に使うのはクラスをインポートする機能に
なるでしょう。
この部分ですね。
JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit");
サンプル。
src/test/java/org/littlewings/archunit/ImportClassesTest.java
package org.littlewings.archunit; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class ImportClassesTest { @Test public void importPackages() { JavaClasses classes = new ClassFileImporter().importPackages("org.littlewings.archunit"); assertThat(classes.stream().anyMatch(cls -> cls.getSimpleName().equals("JaxrsActivator"))).isTrue(); assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Resource"))).isTrue(); assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Service"))).isTrue(); assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Test"))).isTrue(); } @Test public void importPackagesUsingPredefinedImportOptions() { JavaClasses classes = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) .importPackages("org.littlewings.archunit"); assertThat(classes.stream().anyMatch(cls -> cls.getSimpleName().equals("JaxrsActivator"))).isTrue(); assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Resource"))).isTrue(); assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Service"))).isTrue(); assertThat(classes.stream().anyMatch(cls -> cls.getName().endsWith("Test"))).isFalse(); // falase } }
クラスをインポートする際に、オプションを指定することもできます。
以下では、JARファイルは対象外、テストクラスも対象外にしています。
JavaClasses classes = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) .importPackages("org.littlewings.archunit");
Lang API
Lang APIは、プリミティブなCore APIの表現力不足を補うものです。
以下のようなものがLang APIですね。
// serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可 ArchRule rule = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service..");
Library API
Library APIでは、アーキテクチャーを定義してチェックすることができます。
レイヤーを定義して、レイヤー間の依存関係やサイクルチェックなどができます。
Lang APIをもっと見る
Lang APIが以下のような部分だと書きましたが、見ると意味はなんとなくわかるのですが、いざ書こうとすると自信がなくなって
けっこう困ります。
// serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみの依存関係を許可 ArchRule rule = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service.."); // serviceパッケージ内(サブパッケージ含む)は、controllerまたはserviceパッケージからのみ呼び出されていること ArchRule rule = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");
これを少し分解して見ていきたいと思います。
以下の3つに分解して読めば良さそうです。
ArchRuleDefinition.classes() .that().resideInAnyPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");
どう読むかというと。
以下が、「なにが対象か?」を指しています。たとえば、クラスやフィールド、メソッドなどです。
ArchRuleDefinition.classes()
ArchRuleDefinition (archunit 1.0.1 API)
classes
、constructors
、members
、fields
、methods
、codeUnits
などを選びます。これらは、対象としたものが
「存在すること」を表します。
noClasses
、noConstructors
、noMembers
、noFields
、noMethods
、noCodeUnits
といったno
で始まるものもあり、これらは
「存在しないこと」を表します。
次に、こちらで範囲の絞り込みを行います。
.that().resideInAnyPackage("..service..")
that
で始まり、対象にした要素から以下のインターフェースで絞り込みを行います。どういう絞り込みができるのかは、
各インターフェースのJavadocを読んだ方がよいでしょう。
先ほどの例では、service
パッケージ(サブクラスを含む)という意味になります。
この..service..
という表記は、PackageMatcher
に説明があります。
'..pack..' matches 'a.pack', 'a.pack.b' or 'a.b.pack.c.d', but not 'a.packa.b'
PackageMatcher (archunit 1.0.1 API)
記法は他にもいくつかります。
そして、こちらがチェックの内容です。
.should().onlyBeAccessed().byAnyPackage("..resource..", "..service..");
should
で始まり、以降にチェックの内容が続きます。対象にした要素から以下のインターフェースでチェックの内容を定義します。
こちらも、どのようなチェックができるかは各インターフェースのJavadocを読んだ方がよいでしょう。
サンプル
というわけで、もう少しサンプルを書いてみましょう。
src/test/java/org/littlewings/archunit/ArchUnitExampleTest.java
package org.littlewings.archunit; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; import org.junit.jupiter.api.Test; public class ArchUnitExampleTest { static JavaClasses targetClasses = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) .importPackages("org.littlewings.archunit"); @Test public void naming() { // resourceパッケージ内(サブパッケージ含む)のクラス名は、「Resource」で終わること ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..") .should().haveSimpleNameEndingWith("Resource") .check(targetClasses); // serviceパッケージ内(サブパッケージ含む)のクラス名は、「Service」で終わること ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().haveSimpleNameEndingWith("Service") .check(targetClasses); // repositoryパッケージ内(サブパッケージ含む)のクラス名は、「Repository」で終わること ArchRuleDefinition.classes().that().resideInAnyPackage("..repository..") .should().haveSimpleNameEndingWith("Repository") .check(targetClasses); } @Test public void beanAnnotatedApplicationScoped() { // resource、service、repositoryパッケージ内(サブパッケージ含む)のクラスには、@ApplicationScopedアノテーションが付与されていること ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..", "..service..", "..repository..") .should().beAnnotatedWith(ApplicationScoped.class) .check(targetClasses); } @Test public void serviceAnnotatedTransactional() { // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのみ ArchRuleDefinition.classes().that().areAnnotatedWith(Transactional.class) .should().resideInAnyPackage("..service..") .check(targetClasses); // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのメソッドのみ // メソッドでの適用がなくてもOKとする ArchRuleDefinition.methods().that().areAnnotatedWith(Transactional.class) .should().beDeclaredInClassesThat().resideInAnyPackage("..service..") .allowEmptyShould(true) .check(targetClasses); // serviceパッケージ内(サブパッケージ含む)のクラスには、@Transactionalアノテーションが付与されていること ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().beAnnotatedWith(Transactional.class) .check(targetClasses);; } }
クラスのインポート。
static JavaClasses targetClasses = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) .importPackages("org.littlewings.archunit");
命名規則のチェック。
@Test public void naming() { // resourceパッケージ内(サブパッケージ含む)のクラス名は、「Resource」で終わること ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..") .should().haveSimpleNameEndingWith("Resource") .check(targetClasses); // serviceパッケージ内(サブパッケージ含む)のクラス名は、「Service」で終わること ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().haveSimpleNameEndingWith("Service") .check(targetClasses); // repositoryパッケージ内(サブパッケージ含む)のクラス名は、「Repository」で終わること ArchRuleDefinition.classes().that().resideInAnyPackage("..repository..") .should().haveSimpleNameEndingWith("Repository") .check(targetClasses); }
@ApplicationScoped
アノテーションが各パッケージのクラスに付与されていることのチェック。
@Test public void beanAnnotatedApplicationScoped() { // resource、service、repositoryパッケージ内(サブパッケージ含む)のクラスには、@ApplicationScopedアノテーションが付与されていること ArchRuleDefinition.classes().that().resideInAnyPackage("..resource..", "..service..", "..repository..") .should().beAnnotatedWith(ApplicationScoped.class) .check(targetClasses); }
@Transactional
アノテーションは、serviceパッケージ内のクラスのみに付与されていることのチェック。
@Test public void serviceAnnotatedTransactional() { // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのみ ArchRuleDefinition.classes().that().areAnnotatedWith(Transactional.class) .should().resideInAnyPackage("..service..") .check(targetClasses); // @Transactionalアノテーションが付与されてOKなのは、serviceパッケージ内(サブパッケージ含む)のクラスのメソッドのみ // メソッドでの適用がなくてもOKとする ArchRuleDefinition.methods().that().areAnnotatedWith(Transactional.class) .should().beDeclaredInClassesThat().resideInAnyPackage("..service..") .allowEmptyShould(true) .check(targetClasses); // serviceパッケージ内(サブパッケージ含む)のクラスには、@Transactionalアノテーションが付与されていること ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().beAnnotatedWith(Transactional.class) .check(targetClasses);; }
こんな感じですね。
Library API
Library APIを使うと、レイヤーを定義したりして、さらに高い抽象度でのチェックができるようになります。
こんな感じですね。
src/test/java/org/littlewings/archunit/LayeredTest.java
package org.littlewings.archunit; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.library.Architectures; import org.junit.jupiter.api.Test; public class LayeredTest { @Test public void layeredAccess() { JavaClasses targetClasses = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) .importPackages("org.littlewings.archunit"); Architectures.LayeredArchitecture layeredArchitecture = Architectures.layeredArchitecture() .consideringAllDependencies() .layer("Resource").definedBy("..resource..") .layer("Service").definedBy("..service..") .layer("Repository").definedBy("..repository..") .whereLayer("Resource").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Resource", "Service") .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service"); layeredArchitecture.check(targetClasses); } }
各パッケージに「レイヤー」の名前を付け、その依存関係のチェックを定義しています。
Architectures.LayeredArchitecture layeredArchitecture = Architectures.layeredArchitecture() .consideringAllDependencies() .layer("Resource").definedBy("..resource..") .layer("Service").definedBy("..service..") .layer("Repository").definedBy("..repository..") .whereLayer("Resource").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Resource", "Service") .whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
APIとしては、このあたりを使います。
- Architectures (archunit 1.0.1 API)
- Architectures.LayeredArchitecture (archunit 1.0.1 API)
- Architectures.LayeredArchitecture.DependencySettings (archunit 1.0.1 API)
この例では、Resource
レイヤーはどのレイヤーからもアクセスされてはいけない、
Service
レイヤーはResource
およびService
レイヤーからのアクセスのみ許可、Repository
レイヤーやService
レイヤーからの
アクセスのみ許可、といった感じです。
クラスのインポートの部分については、他と同じですね。
JUnitサポートを試す
最後に、ArchUnitのJUnitサポートを試してみます。
最初に書いたサンプルを、JUnitサポートを使って書き直してみました。
src/test/java/org/littlewings/archunit/ArchUnitHelloWorldJUnit5SupportTest.java
package org.littlewings.archunit; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; @AnalyzeClasses( packages = "org.littlewings.archunit", importOptions = {ImportOption.DoNotIncludeJars.class, ImportOption.DoNotIncludeTests.class} ) public class ArchUnitHelloWorldJUnit5SupportTest { @ArchTest public static ArchRule onlyDependentServiceFromResource = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service.."); @ArchTest public static void onlyAccessServiceFromResource(JavaClasses classes) { ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..") .check(classes); } }
クラスのインポートを@AnalyzeClasses
で
@AnalyzeClasses( packages = "org.littlewings.archunit", importOptions = {ImportOption.DoNotIncludeJars.class, ImportOption.DoNotIncludeTests.class} ) public class ArchUnitHelloWorldJUnit5SupportTest {
ルールの定義を@ArchTest
を付与したstaticフィールドまたはメソッドで定義できます。
@ArchTest public static ArchRule onlyDependentServiceFromResource = ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyHaveDependentClassesThat().resideInAnyPackage("..resource..", "..service.."); @ArchTest public static void onlyAccessServiceFromResource(JavaClasses classes) { ArchRuleDefinition.classes().that().resideInAnyPackage("..service..") .should().onlyBeAccessed().byAnyPackage("..resource..", "..service..") .check(classes); }
staticフィールドで定義した場合は、@AnalyzeClasses
で指定したクラスを対象にチェックの実行までを行ってくれます。
こんなところでしょうか。
まとめ
ArchUnitを試してみました。
実際にソースコードを書く時に、パッケージ間の依存関係やネーミングなど、ルールとしてチェックしたいものを定義、テストできて
よいですね。
Lang APIにちょっと迷いましたが、慣れればなんとかなりそうなので、使えるところでは使っていこうかなと思います。