Slide 1

Slide 1 text

ArchUnit で始める Java アプリケーション アーキテクチャの自動テスト 自動化大好きエンジニアLT会 @kawanamiyuu

Slide 2

Slide 2 text

自己紹介 ● かわなみゆう ● @kawanamiyuu ● 株式会社ラクス / Lead Engineer ● 人事・労務業務を楽にする SaaS の開発 ● Spring Boot / Doma / Vue.js / Puppeteer / GitLab CI ● 自動テスト(が書きやすい設計を考えながらコード) を書くのがすき 2

Slide 3

Slide 3 text

アーキテクチャテストに関する登壇資料 3 https://speakerdeck.com/kawanamiyuu/object-oriented-conference-2020 https://speakerdeck.com/kawanamiyuu/jjug-ccc-2020-fall https://speakerdeck.com/kawanamiyuu/jjug-ccc-2019-spring

Slide 4

Slide 4 text

ArchUnit とは 4

Slide 5

Slide 5 text

5 https://www.archunit.org/

Slide 6

Slide 6 text

ArchUnit を一言でいうと ● Java(や Kotlin, Scala)で書かれたアプリケーションのパッ ケージやクラスの依存関係を JUnit のテストコードとして表現 し、テストできるテストフレームワーク 6 Layered Architecture Onion Architecture (Clean Architecture) Domain Model

Slide 7

Slide 7 text

ArchUnit を一言でいうと ● Java(や Kotlin, Scala)で書かれたアプリケーションのパッ ケージやクラスの依存関係を JUnit のテストコードとして表現 し、テストできるテストフレームワーク ● 依存関係の他にも、そのアプリケーション固有の実装ルール もテストすることができる ● 技術力にばらつきがある開発チームで、一定の強制力をもっ てアーキテクチャの設計品質を担保できる 7

Slide 8

Slide 8 text

ArchUnit を一言でいうと ● Java(や Kotlin, Scala)で書かれたアプリケーションのパッ ケージやクラスの依存関係を JUnit のテストコードとして表現 し、テストできるテストフレームワーク ● 依存関係の他にも、そのアプリケーション固有の実装ルール もテストすることができる ● 技術力にばらつきがある開発チームで、一定の強制力をもっ てアーキテクチャの設計品質を担保できる 8

Slide 9

Slide 9 text

アーキテクチャテストの例 9

Slide 10

Slide 10 text

(1) Layered Architecture 10

Slide 11

Slide 11 text

11 @Test void DIP_依存性逆転の原則_を適用したレイヤードアーキテクチャ () { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 12

Slide 12 text

12 @Test void DIP_依存性逆転の原則_を適用したレイヤードアーキテクチャ () { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 13

Slide 13 text

13 @Test void DIP_依存性逆転の原則_を適用したレイヤードアーキテクチャ () { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 14

Slide 14 text

14 @Test void DIP_依存性逆転の原則_を適用したレイヤードアーキテクチャ () { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 15

Slide 15 text

15 @Test void DIP_依存性逆転の原則_を適用したレイヤードアーキテクチャ () { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 16

Slide 16 text

16 @Test void DIP_依存性逆転の原則_を適用したレイヤードアーキテクチャ () { layeredArchitecture() .layer("ui").definedBy("com.example.presentation..") .layer("app").definedBy("com.example.application..") .layer("domain").definedBy("com.example.domain..") .layer("infra").definedBy("com.example.infrastructure..") .whereLayer("ui").mayOnlyBeAccessedByLayers("infra") .whereLayer("app").mayOnlyBeAccessedByLayers("infra", "ui") .whereLayer("domain").mayOnlyBeAccessedByLayers("infra", "app") .whereLayer("infra").mayNotBeAccessedByAnyLayer() .check(CLASSES); }

Slide 17

Slide 17 text

$ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ () FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule Layered architecture consisting of layer 'ui' ('com.example.presentation..') layer 'app' ('com.example.application..') layer 'domain' ('com.example.domain..') layer 'infra' ('com.example.infrastructure..') where layer 'ui' may only be accessed by layers ['infra'] where layer 'app' may only be accessed by layers ['infra', 'ui'] where layer 'domain' may only be accessed by layers ['infra', 'app'] where layer 'infra' may not be accessed by any layer was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:178) at com.example.ArchitectureTest.DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ (ArchitectureTest.java:28) 17

Slide 18

Slide 18 text

$ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ () FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule Layered architecture consisting of layer 'ui' ('com.example.presentation..') layer 'app' ('com.example.application..' layer 'domain' ('com.example.domain..') layer 'infra' ('com.example.infrastructure..') where layer 'ui' may only be accessed by layers ['infra'] where layer 'app' may only be accessed by layers ['infra', 'ui'] where layer 'domain' may only be accessed by layers ['infra', 'app'] where layer 'infra' may not be accessed by any layer was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:178) at com.example.ArchitectureTest.DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ (ArchitectureTest.java:28) 18

Slide 19

Slide 19 text

$ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ () FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule Layered architecture consisting of layer 'ui' ('com.example.presentation..') layer 'app' ('com.example.application..' layer 'domain' ('com.example.domain..') layer 'infra' ('com.example.infrastructure..') where layer 'ui' may only be accessed by layers ['infra'] where layer 'app' may only be accessed by layers ['infra', 'ui'] where layer 'domain' may only be accessed by layers ['infra', 'app'] where layer 'infra' may not be accessed by any layer was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:178) at com.example.ArchitectureTest.DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ (ArchitectureTest.java:28) 19

Slide 20

Slide 20 text

$ ./gradlew clean test > Task :test FAILED com.example.ArchitectureTest > DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ () FAILED java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule Layered architecture consisting of layer 'ui' ('com.example.presentation..') layer 'app' ('com.example.application..' layer 'domain' ('com.example.domain..') layer 'infra' ('com.example.infrastructure..') where layer 'ui' may only be accessed by layers ['infra'] where layer 'app' may only be accessed by layers ['infra', 'ui'] where layer 'domain' may only be accessed by layers ['infra', 'app'] where layer 'infra' may not be accessed by any layer was violated (2 times): Constructor (com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type in (EmployeeService.java:0) Field has type in (EmployeeService.java:0) at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:91) at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:81) at com.tngtech.archunit.library.Architectures$LayeredArchitecture.check(Architectures.java:178) at com.example.ArchitectureTest.DIP_依存性逆転の原則_を適用したレイヤーアーキテクチャ (ArchitectureTest.java:28) 20

Slide 21

Slide 21 text

(2) パッケージ間の依存関係 21

Slide 22

Slide 22 text

@Test void UI層のクラスはインフラストラクチャ層のクラスからのみ依存される() { classes().that().resideInAPackage("com.example.presentation..") .should() .onlyHaveDependentClassesThat().resideInAPackage("com.example.infrastructure..") .check(CLASSES); } @Test void ドメイン層のクラスは他の層のクラスに依存しない() { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAnyPackage( "com.example.presentation..", "com.example.application..", "com.example.infrastructure..") .check(CLASSES); } 22

Slide 23

Slide 23 text

@Test void UI層のクラスはインフラストラクチャ層のクラスからのみ依存される() { classes().that().resideInAPackage("com.example.presentation..") .should() .onlyHaveDependentClassesThat().resideInAPackage("com.example.infrastructure..") .check(CLASSES); } @Test void ドメイン層のクラスは他の層のクラスに依存しない() { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAnyPackage( "com.example.presentation..", "com.example.application..", "com.example.infrastructure..") .check(CLASSES); } 23

Slide 24

Slide 24 text

@Test void UI層のクラスはインフラストラクチャ層のクラスからのみ依存される() { classes().that().resideInAPackage("com.example.presentation..") .should() .onlyHaveDependentClassesThat().resideInAPackage("com.example.infrastructure..") .check(CLASSES); } @Test void ドメイン層のクラスは他の層のクラスに依存しない() { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAnyPackage( "com.example.presentation..", "com.example.application..", "com.example.infrastructure..") .check(CLASSES); } 24

Slide 25

Slide 25 text

(3) 実行環境・フレームワークへの依存 25

Slide 26

Slide 26 text

26 @Test void ドメイン層はWeb実行環境に依存しない () { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAPackage("javax.servlet..") .check(CLASSES); } @Test void ドメイン層はWebアプリケーションフレームワークに依存しない () { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAPackage("org.springframework..") .check(CLASSES); }

Slide 27

Slide 27 text

27 @Test void ドメイン層はWeb実行環境に依存しない () { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAPackage("javax.servlet..") .check(CLASSES); } @Test void ドメイン層はWebアプリケーションフレームワークに依存しない () { noClasses().that().resideInAPackage("com.example.domain..") .should() .dependOnClassesThat().resideInAPackage("org.springframework..") .check(CLASSES); }

Slide 28

Slide 28 text

(4) アプリケーション固有の実装ルール 28

Slide 29

Slide 29 text

@Test void APIエンドポイントではユーザの権限を必ずチェックする () { methods().that(new DescribedPredicate<>("コントローラーのハンドラーメソッド ") { @Override public boolean apply(JavaMethod method) { return method.isAnnotatedWith(RequestMapping.class) || method.isAnnotatedWith(GetMapping.class) || method.isAnnotatedWith(PostMapping.class) || method.isAnnotatedWith(PutMapping.class) || method.isAnnotatedWith(PatchMapping.class) || method.isAnnotatedWith(DeleteMapping.class); } }) .should(new ArchCondition<>("@Role でアノテートする") { @Override public void check(JavaMethod method, ConditionEvents events) { if (! method.isAnnotatedWith(Role.class)) { // 実装違反を通知 events.add(SimpleConditionEvent.violated(method.getOwner(), String.format("%s is not annotated with @Role.", method.getFullName()) )); } } }) .check(CLASSES); } 29

Slide 30

Slide 30 text

@Test void APIエンドポイントではユーザの権限を必ずチェックする () { methods().that(new DescribedPredicate<>("コントローラーのハンドラーメソッド ") { @Override public boolean apply(JavaMethod method) { return method.isAnnotatedWith(RequestMapping.class) || method.isAnnotatedWith(GetMapping.class) || method.isAnnotatedWith(PostMapping.class) || method.isAnnotatedWith(PutMapping.class) || method.isAnnotatedWith(PatchMapping.class) || method.isAnnotatedWith(DeleteMapping.class); } }) .should(new ArchCondition<>("@Role でアノテートする") { @Override public void check(JavaMethod method, ConditionEvents events) { if (! method.isAnnotatedWith(Role.class)) { // 実装違反を通知 events.add(SimpleConditionEvent.violated(method.getOwner(), String.format("%s is not annotated with @Role.", method.getFullName()) )); } } }) .check(CLASSES); } 30

Slide 31

Slide 31 text

まとめ 31

Slide 32

Slide 32 text

まとめ ● ArchUnit は、レイヤードアーキテクチャのような形の定まった アーキテクチャのテストから、アプリケーション固有の実装 ルールのテストまで、柔軟に活用できる ● アーキテクチャテストに興味が湧いた方はぜひ、冒頭でご紹 介した登壇資料をご覧ください! 32

Slide 33

Slide 33 text

Appendix. 33

Slide 34

Slide 34 text

about ArchUnit ● GitHub ○ https://github.com/TNG/ArchUnit ○ https://github.com/TNG/ArchUnit-Examples ● Twitter ○ https://twitter.com/archtests ● Technology Radar (2018) ○ https://www.thoughtworks.com/radar/tools/archunit ○ 進化的アーキテクチャ x 適応度関数 34

Slide 35

Slide 35 text

他のプログラミング言語でのアーキテクチャテスト ● TNG/ArchUnitNET(C#) ● iternity/archlint.cs(C#) ● BenMorris/NetArchTest(.Net) ● sensiolabs-de/deptrac(PHP) ● carlosas/phpat(PHP) ● nazonohito51/dependency-analyzer(PHP) 35