Search code examples
javaspringspring-bootaopaspectj

How can I use an Aspect on a SpringBootTest


I have an aspect that I have created that does not work when utilised directly on a Test method, but does work when added to a helper component. How can I get this working when I use it directly.

So in the code below testAspect fails, but testAspectHelper succeeds.

A breakpoint within the aspect shows the code isn't hit in the failing test, but is hit in the test that passes.

import org.junit.jupiter.api.Test;
import org.opennms.horizon.inventory.SpringContextTestInitializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest
@ContextConfiguration(initializers = {SpringContextTestInitializer.class})
@AutoConfigureObservability
public class AspectTest {
    @Autowired
    AspectTestHelper aspectTestHelper;

    @Test
    @WithTenant(tenantId = "Fred")
    public void testAspect() {
        // Test fails
        assert("Fred".equals(TenantContext.getTenantId()));
    }

    @Test
    public void testAspectHelper() {
        // Test succeeds
        aspectTestHelper.setTenant();
    }
}

import org.springframework.stereotype.Component;

@Component
public class AspectTestHelper {
    @WithTenant(tenantId = "Alex")
    public void setTenant() {
        assert("Alex".equals(TenantContext.getTenantId()));
    }
}

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 WithTenant {
    String tenantId() default "";
    int tenantIdArg() default -1;
    String tenantIdArgInternalMethod() default "";
    String tenantIdArgInternalClass() default "";
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
@Aspect
public class WithTenantAspect {
    private static final Logger LOG = LoggerFactory.getLogger(WithTenantAspect.class);

    @Autowired
    private TenantLookup tenantLookup;

    @Around(("@annotation(withTenant)"))
    public Object getTenant(ProceedingJoinPoint joinPoint, WithTenant withTenant) throws Throwable {
        String tenantId = withTenant.tenantId();
        int tenantIdArg = withTenant.tenantIdArg();
        String tenantIdArgInternalMethod = withTenant.tenantIdArgInternalMethod();
        String tenantIdArgInternalClass = withTenant.tenantIdArgInternalClass();

        if (tenantIdArg >= 0) {
            Object[] args = joinPoint.getArgs();
            if (args.length <= tenantIdArg) {
                throw new RuntimeException("TenantIdArg position is greater than the number of arguments to the method");
            }
            if (tenantIdArgInternalMethod == null || tenantIdArgInternalMethod.isEmpty() || tenantIdArgInternalClass == null || tenantIdArgInternalClass.isEmpty()) {
                tenantId = String.valueOf(args[tenantIdArg]);
            } else {
                Object tenantObj = args[tenantIdArg];
                Class clazz = Class.forName(tenantIdArgInternalClass);
                Method method = clazz.getMethod(tenantIdArgInternalMethod);
                Object tenant = method.invoke(tenantObj);
                tenantId = String.valueOf(tenant);
            }
        }

        if (tenantId == null || tenantId.isEmpty()) {
            tenantId = tenantLookup.lookupTenantId().orElseThrow();
        }

        try {
            TenantContext.setTenantId(tenantId);
            Object proceed = joinPoint.proceed();
            return proceed;
        } finally {
            TenantContext.clear();
        }
    }
}


Solution

  • I also recommend a separate @Component to be used in the test. You can, however, do something contrived in order to get it working with a method annotated in the test directly:

    • Add a @Component annotation to the test class or a corresponding XML <bean/> definition.
    • Add @Autowired @Lazy private AspectTest aspectTest; to your test.
    • Annotate a helper method within the test class, which is not a @Test.
    • Call that method from a @Test like so: aspectTest.myHelper().

    Now the aspect will be triggered.

    I am not recommending this, it is super ugly. But it works and shows you that

    • Spring AOP aspects only work on Spring components (native AspectJ aspects have no such limitations),
    • self-invocation does not work on Spring proxies, i.e. you need the trick to auto-wire a proxy instance into its own class and use that proxy to call methods, if you want Spring AOP to kick in (again, in native AspectJ this would be unnecessary).