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

73560128b23de542e47a318145bc781a?s=47 Yu Kawanami
November 18, 2020

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

「自動化大好きエンジニアLT会」の発表資料
https://rakus.connpass.com/event/192101/

73560128b23de542e47a318145bc781a?s=128

Yu Kawanami

November 18, 2020
Tweet

Transcript

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

  2. 自己紹介 • かわなみゆう • @kawanamiyuu • 株式会社ラクス / Lead Engineer

    • 人事・労務業務を楽にする SaaS の開発 • Spring Boot / Doma / Vue.js / Puppeteer / GitLab CI • 自動テスト(が書きやすい設計を考えながらコード) を書くのがすき 2
  3. アーキテクチャテストに関する登壇資料 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

  4. ArchUnit とは 4

  5. 5 https://www.archunit.org/

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

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

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

    • 依存関係の他にも、そのアプリケーション固有の実装ルール もテストすることができる • 技術力にばらつきがある開発チームで、一定の強制力をもっ てアーキテクチャの設計品質を担保できる 8
  9. アーキテクチャテストの例 9

  10. (1) Layered Architecture 10

  11. 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); }
  12. 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); }
  13. 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); }
  14. 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); }
  15. 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); }
  16. 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); }
  17. $ ./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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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
  18. $ ./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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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
  19. $ ./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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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
  20. $ ./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.domain.employee.EmployeeService.<init>(com.example.infrastructure.datasource.EmployeeRepositoryImpl)> has parameter of type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> in (EmployeeService.java:0) Field <com.example.domain.employee.EmployeeService.repository> has type <com.example.infrastructure.datasource.EmployeeRepositoryImpl> 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
  21. (2) パッケージ間の依存関係 21

  22. @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
  23. @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
  24. @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
  25. (3) 実行環境・フレームワークへの依存 25

  26. 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); }
  27. 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); }
  28. (4) アプリケーション固有の実装ルール 28

  29. @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
  30. @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
  31. まとめ 31

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

  33. Appendix. 33

  34. 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
  35. 他のプログラミング言語でのアーキテクチャテスト • TNG/ArchUnitNET(C#) • iternity/archlint.cs(C#) • BenMorris/NetArchTest(.Net) • sensiolabs-de/deptrac(PHP) •

    carlosas/phpat(PHP) • nazonohito51/dependency-analyzer(PHP) 35