0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

アーキテクチャテストAdvent Calendar 2020

Day 18

ArchUnit 実践:使用範囲が限定されたメソッドの可視性をパッケージプライベートまたはプライベートに強制する

Last updated at Posted at 2020-12-18
// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1

アーキテクチャテストのモチベーション

16 日目の ArchUnit 実践:同一パッケージからのみ呼び出されるメソッドの可視性をパッケージプライベートまたはプライベートに強制する と 17 日目の ArchUnit 実践:自クラスからのみ呼び出されるメソッドの可視性をプライベートに強制する の 2 つのアーキテクチャテストの対象となるメソッド群は包含関係(自クラスからのみ呼び出されるメソッド ⊆ 同一パッケージからのみ呼び出されるメソッド)にあります。
2 つのテストを同時に実行すると両方のテストで同じメソッドが違反として検出されテスト結果の精査が手間になる可能性があるため、1 つのテストにまとめてみます。

アーキテクチャテストの実装

package com.example;
 
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAccess;
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.JavaModifier;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.jupiter.api.Test;

import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;

class ArchitectureTest {

    // 検査対象のクラス
    private static final JavaClasses CLASSES =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages("com.example");

    @Test
    void 使用範囲が限定されたメソッドの可視性をパッケージプライベートまたはプライベートに強制する() {
        methods()
            .should(new ArchCondition<>("be package private or be private, if the scope of use is limited") {
                @Override
                public void check(final JavaMethod input, final ConditionEvents events) {
                    Method method = new Method(input);

                    if (! method.isPrivate() 
                        && method.isOnlyCalledInDeclaredClass()
                    ) {
                        // 自クラスからのみ呼ばれるメソッドはプライベートであるべき
                        events.add(SimpleConditionEvent.violated(input,
                            String.format("`%s` should be private.", input.getFullName())));

                    } else if (! method.isPackagePrivate() 
                        && method.isOnlyCalledFromClassesInSamePackage()
                    ) {
                        // 同一パッケージからのみ呼ばれるメソッドはパッケージプライベートであるべき
                        events.add(SimpleConditionEvent.violated(input,
                            String.format("`%s` should be package private.", input.getFullName())));
                    }
                }
            })
            .check(CLASSES);
    }

    class Method {
        private final Set<JavaModifier> modifiers;
        private final JavaClass ownerClass;
        private final Set<JavaClass> callerClasses;

        Method(final JavaMethod method) {
            modifiers = method.getModifiers();
            ownerClass = method.getOwner();
            callerClasses = method.getAccessesToSelf()
                .stream()
                .map(JavaAccess::getOriginOwner)
                .collect(Collectors.toSet());
        }

        boolean isPrivate() {
            return modifiers.contains(JavaModifier.PRIVATE);
        }

        boolean isPackagePrivate() {
            return ! modifiers.contains(JavaModifier.PUBLIC)
                && ! modifiers.contains(JavaModifier.PROTECTED)
                && ! modifiers.contains(JavaModifier.PRIVATE);
        }

        boolean isOnlyCalledInDeclaredClass() {
            return callerClasses
                .stream()
                .allMatch(callerClass -> callerClass.isEquivalentTo(ownerClass.reflect()));
        }

        boolean isOnlyCalledFromClassesInSamePackage() {
            return callerClasses
                .stream()
                .allMatch(callerClass -> callerClass.getPackageName().equals(ownerClass.getPackageName()));
        }
    }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?