1

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();
        }
    }
}

James Hutchinson
  • 841
  • 2
  • 13
  • 27

2 Answers2

1

An Aspect acts as a proxy and only triggers when the function it’s set on is called from another class. If you have a function annotated with @WithTennant in a regular class and call that function from another function within the same class it also won’t trigger the aspect. Unless you want to test using an implementation in your code directly, I don’t see how you could do it without a helper class like you did.

I.Brok
  • 319
  • 1
  • 2
  • 16
1

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).
kriegaex
  • 63,017
  • 15
  • 111
  • 202