Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ArchUnit で始める Java アプリケーションアーキテクチャの自動テスト / 自動化大...

Yu Kawanami
November 18, 2020

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

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

Yu Kawanami

November 18, 2020
Tweet

More Decks by Yu Kawanami

Other Decks in Technology

Transcript

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

    • 人事・労務業務を楽にする SaaS の開発 • Spring Boot / Doma / Vue.js / Puppeteer / GitLab CI • 自動テスト(が書きやすい設計を考えながらコード) を書くのがすき 2
  2. ArchUnit を一言でいうと • Java(や Kotlin, Scala)で書かれたアプリケーションのパッ ケージやクラスの依存関係を JUnit のテストコードとして表現 し、テストできるテストフレームワーク

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

    • 依存関係の他にも、そのアプリケーション固有の実装ルール もテストすることができる • 技術力にばらつきがある開発チームで、一定の強制力をもっ てアーキテクチャの設計品質を担保できる 8
  4. 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); }
  5. 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); }
  6. 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); }
  7. 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); }
  8. 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); }
  9. 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); }
  10. $ ./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
  11. $ ./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
  12. $ ./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
  13. $ ./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
  14. @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
  15. @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
  16. @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
  17. 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); }
  18. 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); }
  19. @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
  20. @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
  21. 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