Search code examples
javaspring-webfluxspring-aopspock

Spock -Unit Test:How to write spock unit test for @around annotation which takes Mono


Hi I am using following code to print logs using aop in my webflux app,I have trouble writing unit/integration tests ?can we verify log interactions here?Any help would be appreciated

@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.METHOD) 
public @interface Loggable {} 
@Aspect
@Slf4j
public class LoggerAspect {

  @Around("@annotation(Loggable)")
  public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {

    long start = System.currentTimeMillis();
    var result = joinPoint.proceed();
    if (result instanceof Mono) {
      var monoResult = (Mono) result;
      AtomicReference<String> traceId = new AtomicReference<>("");

      return monoResult
        .doOnSuccess(o -> {
          var response = "";
          if (Objects.nonNull(o)) {
            response = o.toString();
          }
          log.info("Enter: {}.{}() with argument[s] = {}",
            joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
            joinPoint.getArgs());
          log.info("Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
            joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
            joinPoint.getArgs()[0],
            response, (System.currentTimeMillis() - start));
        });
    }
  }
}

Test Failing. Somehow when i debug pointer is not going inside doOnNext Method.And I am not sure how I can assert log interaction in above Logging aspect.In Junit5 I know I can use mockito for each method and return something ,but how i can rerturn in spock.

class LogAspectTest extends Specification {
  private static final String MOCK_METHOD_LOG_VALUE = "mockMethodLogValue"
  private Logger log = Mock()
  private ProceedingJoinPoint mockJoinPoint = Mock()
  private static Mono<String> methodReturn = Mono.just(["Data", "Data"])
  private LogAspect logAspect = new LogAspect(log)

  @Unroll
  def 'logAround verify log interaction'() {
    given:
    mockJoinPoint.proceed() == Mono.just("Hello")
    final Method method = TestClass.class.getMethod("mockMethod")

    when:
    logAspect.logAround(mockJoinPoint)

    then:
    interaction { mockJoinPointAndMethodSignatureInteractions(method, methodReturnToUse) }

    where:
    resultType | methodReturnToUse
    'Mono'     | methodReturn
  }

  private void mockJoinPointAndMethodSignatureInteractions(Method method, Publisher result) {
    1 * mockJoinPoint.proceed() >> result
    1 * log.info() >> ""

  }

  private static class TestClass {
    @Loggable
    Mono<String> mockMethod() { return Mono.just("data") }

  }
}

Is it recommended to write Integration Test for @Loggable annotation since it just logging not sure how can write Integration Test which assert the log statements


Solution

  • Like I said in my comment, you cannot easily mock a private static final field without using add-on tools like PowerMock or similar. I think that whenever you need something like that, you should rather refactor your code for better testability. Here is an idea which is far from perfect, but I want to give you an idea about how you could unit-test your aspect. As for an integration test, you can also do that, but ask yourself what you want to test: really the aspect or that Spring AOP pointcut matching works correctly?

    Anyway, let us assume that your classes under test are:

    package de.scrum_master.stackoverflow.q64164101;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Loggable {}
    
    package de.scrum_master.stackoverflow.q64164101;
    
    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 reactor.core.publisher.Mono;
    
    import java.util.Objects;
    import java.util.function.Consumer;
    
    @Aspect
    public class LogAspect {
      private static final Logger log = LoggerFactory.getLogger(LogAspect.class.getName());
    
      @Around("@annotation(Loggable)")
      public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        if (result instanceof Mono)
          return ((Mono) result).doOnSuccess(getConsumer(joinPoint, start));
        return result;
      }
    
      public Consumer getConsumer(ProceedingJoinPoint joinPoint, long start) {
        return o -> {
          String response = "";
          if (Objects.nonNull(o))
            response = o.toString();
          log.info("Enter: {}.{}() with argument[s] = {}",
            joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
            joinPoint.getArgs());
          log.info("Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
            joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
            joinPoint.getArgs()[0],
            response, (System.currentTimeMillis() - start));
        };
      }
    }
    

    See how I factored out the lambda into a helper method? It has two effects:

    • It makes the logAround(ProceedingJoinPoint) advice method more readable.
    • It permits you to stub the helper method and instead of verifying that logging is done you just verify that the helper method was called for Mono results (and not called for other result types).

    The test in its simplest form could look like this:

    package de.scrum_master.stackoverflow.q64164101
    
    import org.aspectj.lang.ProceedingJoinPoint
    import reactor.core.publisher.Mono
    import spock.lang.Specification
    
    class LogAspectTest extends Specification {
      LogAspect logAspect = Spy()
      ProceedingJoinPoint joinPoint = Mock()
    
      def "aspect target method returns a Mono"() {
        given:
        joinPoint.proceed() >> Mono.just("Hello")
    
        when:
        logAspect.logAround(joinPoint)
    
        then:
        1 * logAspect.getConsumer(joinPoint, _)
      }
    
      def "aspect target method does not return a Mono"() {
        given:
        joinPoint.proceed() >> "dummy"
    
        when:
        logAspect.logAround(joinPoint)
    
        then:
        0 * logAspect.getConsumer(joinPoint, _)
      }
    }
    

    Please note how I use a Spy (i.e. a partial mock based on the original object) in order to selectively stub the helper method.


    Update: An alternative for more integrative testing would be to configure your logging framework to log into a target which you can control and verify, e.g. log into an in-memory database or into a buffer which you can access.