[JAVA] ArchUnit Practice: Enforce visibility of limited-use methods into package private or private

//Execution environment
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1

Architectural test motivation

Day 16 ArchUnit Practice: Enforce visibility of methods called only from the same package to package private or private and Day 17 [ArchUnit Practice: Visibility of methods called only from own class] The two methods that are subject to the architecture test of Force Private (https://qiita.com/drafts/a3dde913894232fa2aa5) are in the inclusion relationship (methods called only from their own class ⊆ methods called only from the same package). If you run two tests at the same time, the same method may be detected as a violation in both tests, which can be a hassle to scrutinize the test results, so try to combine them into one test.

Architecture test implementation

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 {

    //Class to be inspected
    private static final JavaClasses CLASSES =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages("com.example");

    @Test
void Enforce visibility of limited-use methods into package private or private() {
        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()
                    ) {
                        //Methods called only from your class should be private
                        events.add(SimpleConditionEvent.violated(input,
                            String.format("`%s` should be private.", input.getFullName())));

                    } else if (! method.isPackagePrivate() 
                        && method.isOnlyCalledFromClassesInSamePackage()
                    ) {
                        //Methods called only from the same package should be package private
                        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()));
        }
    }
}

Recommended Posts

ArchUnit Practice: Enforce visibility of limited-use methods into package private or private
ArchUnit practice: Enforce visibility of methods called only from the same package to package private or private
ArchUnit practice: Enforce package private visibility of classes that depend only on the same package
ArchUnit practice: Privately enforce visibility of methods called only from your class
ArchUnit Practice: Architecture Testing of Onion Architecture