Search code examples
javaarchunit

apply an ArchUnit test to a class or method


I've defined the following ArchUnit tests that verify

  1. any class or method annotated with Spring's @Transactional annotation must also be annotated with @Service

  2. the annotation @jakarta.transaction.Transactional is not allowed on any class or method

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

class TransactionRules {

    @ArchTest // test rule 1 for classes
    static final ArchRule transactionalServiceClasses = classes()
        .that().areAnnotatedWith(Transactional.class)
        .should().beAnnotatedWith(Service.class);

    @ArchTest // test rule 1 for methods
    static final ArchRule transactionalServiceMethods = methods()
        .that().areAnnotatedWith(Transactional.class)
        .should().beDeclaredInClassesThat().areAnnotatedWith(Service.class);

    @ArchTest // test rule 2 for classes
    static final ArchRule noJakartaTransactionClasses = noClasses()
        .should().beAnnotatedWith(jakarta.transaction.Transactional.class);

    @ArchTest // test rule 2 for methods
    static final ArchRule noJakartaTransactionMethods = noMethods()
        .should().beAnnotatedWith(jakarta.transaction.Transactional.class);
}

I had to write 4 tests because I need separate tests for classes and methods. Is there a way to make this more concise and write a single test that will apply to a class or a method?


Solution

  • For "the" first case

    1. any class or method annotated with Spring's @Transactional annotation must also be annotated with @Service

    (which I think are really two structurally different cases: for @Transactional methods, you want the declaring classes to be @Services, not the methods themselves), I don't see how you could combine them if you want to have classes and methods reported as individual violations in the corresponding cases.

    If you didn't care, but really preferred to combine the rules, you could in this case (when you also accept that, for a class annotated with @Transactional but not @Service, you'll get a violation for every method – also none if the class doesn't have any methods) use:

    @ArchTest
    ArchRule transactionalServices = methods()
        .that().areAnnotatedWith(Transactional.class)
        .or().areDeclaredInClassesThat().areAnnotatedWith(Transactional.class)
        .should().beDeclaredInClassesThat().areAnnotatedWith(Service.class);
    

    (I've expressed that I wouldn't do that, haven't I?)


    The second case

    1. the annotation @jakarta.transaction.Transactional is not allowed on any class or method

    is something that could actually be unified as a single rule for classes or methods (regarding their common functionality that they can have annotations [and have a source code location each]), cf. §7.5 Rules with Custom Concepts of the ArchUnit User Guide:

    // Intersection types are tricky, cf. https://stackoverflow.com/q/6643241 .
    // Please let me know if you find a better solution for this:
    <HAS_ANNOTATIONS extends HasAnnotations<?> & HasSourceCodeLocation>
    ClassesTransformer<HAS_ANNOTATIONS> classesOrMethodsWhichCanHaveAnnotations() {
        return new AbstractClassesTransformer<>("classes/methods") {
            @Override
            public Iterable<HAS_ANNOTATIONS> doTransform(JavaClasses classes) {
                List<HAS_ANNOTATIONS> result = new ArrayList(classes); // shortcut 🤫
                classes.forEach(javaClass ->
                    result.addAll((Set<HAS_ANNOTATIONS>) javaClass.getMethods())
                );
                return result;
            }
        };
    }
    
    @ArchTest
    ArchRule noJakartaTransaction = no(classesOrMethodsWhichCanHaveAnnotations())
            .should(beAnnotatedWith(jakarta.transaction.Transactional.class));
    

    This uses the following static imports:

    import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.no;
    import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith;