Unit-test a method that is Advised by @Around advi

2019-07-14 03:46发布

问题:

I'm currently writing JUnit unit-tests for a class in an Application that uses Spring and AspectJ. The class under test has a couple public methods that are advised by an around-advice method in an aspect class. The Aspect has a couple of injected fields that turn up null when the advised method is executed, even though I've successfully instantiated those beans in the test application context, and when their methods are called, they throw nullpointerexceptions. Here's a simplified version of the code:

The class to be tested:

public class ClassUnderTest {

    @Inject
    private Foo foo;

    @Audit(StringValue="arg", booleanValue=true)
    public Object advisedMethod() {
        Object ret = new Object();
        //things happen
        return ret;
    }

The Aspect:

@Aspect
@Configurable
public class AuditAspect implements Versionable {

    @Inject
    Foo foo;

    @Inject
    Bar bar;

    @Around("@annotation(Audit)")
    public Object aroundAdvice(ProceedingJoinPoint pjp, Audit audit) {

        // Things happen
        privMethod(arg);
        // Yet other things happen
        Object ret = pjp.proceed();
        // Still more things happen
        return ret;
    }

    private Object privMethod(Object arg) {
        // Things continue to happen.
        // Then this throws a NullPointerException because bar is null.
        bar.publicBarMethod(arg2);
        // Method continues ...
    }
}

The Audit interface:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
    String value() default "";
    boolean bool() default false;
}

The Config file that provides the application context:

import static org.mockito.Mockito.*;

@Configuration
public class ClassUnderTestTestConfig {

    @Bean
    Foo foo() {
        return mock(Foo.class);
    }

    @Bean
    Bar bar() {
        return mock(Bar.class);
    }

    @Bean
    ClassUnderTest classUnderTest() {
        return new ClassUnderTest();
    }

    @Bean
    @DependsOn({"foo", "bar"})
    Aspect aspect() {
        return new Aspect();
    }
}

The Test class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({
    @ContextConfiguration(classes = ClassUnderTestTestConfiguration.class),
    @ContextConfiguration(classes = ClassUnderTest.class)
})
public class ClassUnderTestTest {

    private static final String sessionNumber = "123456";

    @Inject
    Foo foo;

    @Inject
    Bar bar;

    @Inject
    ClassUnderTest classUnderTest;

    @Inject
    Aspect aspect;

    @Test
    public void test() {
        // This call triggers the advice which ends up throwning
        // a NullPointerException.
        classUnderTest.advised();
    }
}

I've also tried making my own Spring proxy and then adding the aspect to it manually as advised in this stack overflow post, by adding this code to the test class:

@Before
public void setUp() {
    DataPointRestWebService target = new DataPointRestWebService();
    AspectJProxyFactory proxyMaker = new AspectJProxyFactory(target);
    proxyMaker.addAspect(auditAspect);
    dataPointRestWebService = proxyMaker.getProxy();
}

However that ends up throwing:

AopConfigException: Advice must be declared inside an aspect type. Offending method 'public java.lang.Object Aspect.aroundAdvice' in class [Aspect]

I find this cryptic because I the Aspect class does have the @Aspect annotation before it, and the class works outside of a test environment.

I'm very new to Spring and AspectJ, so I'm completely open to the notion that I'm going about this all wrong. I've provided dummy code here in hopes of leaving out unhelpful specifics, but also because the working code is proprietary and not mine. If you think I've left out an important detail, let me know and I'll try to add it.

Thanks in advance for any help, and please let me know if I'm leaving out any crucial information.

EDIT:

By request, I've added the full NullPointerException stack trace:

java.lang.NullPointerException
    at com.unifiedcontrol.aspect.AuditAspect.getCaller(AuditAspect.java:265)
    at com.unifiedcontrol.aspect.AuditAspect.ajc$inlineAccessMethod$com_unifiedcontrol_aspect_AuditAspect$com_unifiedcontrol_aspect_AuditAspect$getCaller(AuditAspect.java:1)
    at com.unifiedcontrol.aspect.AuditAspect.aroundAuditAdvice(AuditAspect.java:79)
    at com.unifiedcontrol.server.rest.DataPointRestWebServiceTest.dummyTest(DataPointRestWebServiceTest.java:109)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:233)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:87)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:176)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

Where AuditAspect == Aspect and DataPointRestWebService == ClassUnderTest.

回答1:

Here's what ended up solving our problem:

We changed the following method in the ClassUnderTestTestConfig class:

@Bean
@DependsOn({"foo", "bar"})
Aspect aspect() {
    return new Aspect();
}

to:

@Bean
@DependsOn({"foo", "bar"})
Aspect aspect() {
    return Aspects.aspectOf(Aspect.class);
}

for which we added the following import statement:

import org.aspectj.lang.Aspects;

The original code successfully returned a new Aspect object, however when ClassUnderTest.advisedMethod() was called, the jointpoint was delegated to a different Aspect object that hadn't been injected with non-null foo and bar member. Something about how the Aspects.aspectOf() method works ensures that the Aspect object created by TestConfig is the one that provides advice to the call to advisedMethod().

At the moment I have no idea why this solved the problem. Someone else at work found the solution. I plan to look into it and edit this post with more information, but in the mean time all contributions are welcome.