TNG / ArchUnit

A Java architecture test library, to specify and assert architecture rules in plain Java
http://archunit.org
Apache License 2.0
3.23k stars 298 forks source link

ArchUnitException$ReflectionException when retrieving annotation #120

Closed vincent-fuchs closed 6 years ago

vincent-fuchs commented 6 years ago

Hi,

We have wrapped ArchUnit into a Maven plugin so that the rules can be easily reused across project through Maven config (we're thinking of opensourcing it.. but that's another discussion).

One of the rule we have is to prevent the use of PowerMock. I though the rule was working fine, until today when I found a project that was declaring the rule but was using PowerMock anyway.

Maybe there's something wrong in the way the project is configured, but I didn't investigate much in that direction. Instead, I tried to understand what could be the problem in my rule. This is what it looks like :

public static ArchCondition<JavaClass> notUsePowerMock() {

    return new ArchCondition<JavaClass>(" not use Powermock ") {

        @Override
        public void check(JavaClass testClass, ConditionEvents events) {

            try {
                Optional<RunWith> runWithAnnotation = testClass.tryGetAnnotationOfType(RunWith.class);

                if (runWithAnnotation.isPresent() && runWithAnnotation.get().toString().contains(POWER_MOCK_RUNNER_CLASS_NAME)) {

                    events.add(SimpleConditionEvent.violated(testClass,
                            ArchUtils.POWER_MOCK_VIOLATION_MESSAGE + testClass.getName()));
                }
            }
            catch(RuntimeException e){
                System.out.println("EXCEPTION : "+e);
            }

        }

    };
}

The exception comes when we try to read the annotation (runWithAnnotation.get()) . At runtime, I am getting things like :

EXCEPTION : com.tngtech.archunit.base.ArchUnitException$ReflectionException: java.lang.ClassNotFoundException: org.powermock.modules.junit4.PowerMockRunner
EXCEPTION : com.tngtech.archunit.base.ArchUnitException$ReflectionException: java.lang.ClassNotFoundException: org.springframework.test.context.junit4.SpringRunner

with a stacktrace like :

com.tngtech.archunit.base.ArchUnitException$ReflectionException: java.lang.ClassNotFoundException: org.springframework.test.context.junit4.SpringRunner
at com.tngtech.archunit.core.domain.JavaType$From$AbstractType.resolveClass(JavaType.java:165)
at com.tngtech.archunit.core.domain.AnnotationProxy$JavaClassConversion.convert(AnnotationProxy.java:118)
at com.tngtech.archunit.core.domain.AnnotationProxy$JavaClassConversion.convert(AnnotationProxy.java:109)
at com.tngtech.archunit.core.domain.AnnotationProxy$Conversions.convertIfNecessary(AnnotationProxy.java:320)
at com.tngtech.archunit.core.domain.AnnotationProxy$ToStringHandler.propertyStrings(AnnotationProxy.java:282)
at com.tngtech.archunit.core.domain.AnnotationProxy$ToStringHandler.handle(AnnotationProxy.java:275)
at com.tngtech.archunit.core.domain.AnnotationProxy$AnnotationMethodInvocationHandler.invoke(AnnotationProxy.java:95)
at com.sun.proxy.$Proxy23.toString(Unknown Source)

In the IDE, I get this extra info in debug mode :

 Method threw 'com.tngtech.archunit.base.ArchUnitException$ReflectionException' exception. Cannot evaluate com.sun.proxy.$Proxy23.toString()

These "missing classes" should be here, because the project builds fine apart from this.

The alternative version that works, but that I don't like, as it relies on String comparison and cast :

 Optional<JavaAnnotation> runWithAnnotation = testClass.getAnnotations().stream()
                        .filter(a -> a.getType().getName().contains("RunWith")).findFirst();

                if (runWithAnnotation.isPresent()) {

                    com.tngtech.archunit.base.Optional<Object> runner= runWithAnnotation.get().get("value");

                    if (((JavaClass) runner.get()).getName().contains(POWER_MOCK_RUNNER_CLASS_NAME)) {
                        events.add(SimpleConditionEvent.violated(testClass,
                                ArchUtils.POWER_MOCK_VIOLATION_MESSAGE + testClass.getName()));
                    }

                }  

Is this behavior "normal" ? is there a way to keep a cleaner code and avoid this ?

I initially used v0.5.0 and tried to upgrade to 0.9.1 to see if the issue would magically disappear, but it;'s still there.

codecholeric commented 6 years ago

AFAIK this behavior happens, if you have @RunWith on your classpath, but not the classes referenced by the annotation. Maybe the classpath of your Maven plugin execution does not contain all these classes? Unfortunately yes, if you can't be sure that you have all classes referenced by annotations on the classpath, you can only rely on the unsafe "stringly typed" version, where a Class<?> will be represented by a JavaClass (as in your second example). I don't know any way around this, because if you try to load @RunWith via reflection (which is what happens, if you use tryGetAnnotationOfType(RunWith.class)), then properties like value = Foo.class will be loaded via reflection as well. And if those classes are missing from your classpath, then I don't think there is any way around an exception. Have you tried to add those dependencies (Powermock Runner, Spring JUnit Runner) as dependencies to the plugin execution?

vincent-fuchs commented 6 years ago

Thanks a lot for your quick answer.

I believe I got blinded by a unit test that I had in my plugin code, that was identifying the classes as I expected. But it's possible that I had never seen the rule actually being effective once incorporated in the plugin and ran in a different project.

Declaring powermock dependency in my plugin without scope=test indeed did the trick : now PowermockRunner is part of the packaged plugin, and visible at runtime.