Spock单元测试:如何为接受Mono的@around注解编写Spock单元测试。

huangapple go评论73阅读模式
英文:

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

英文:

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

答案1

得分: 3

正如我在我的评论中所说,你不能轻松地模拟private static final字段,除非使用像PowerMock或类似的附加工具。我认为每当你需要类似这样的功能时,你应该考虑重构你的代码以提高测试性。这里有一个不完美但可以给你关于如何单元测试你的切面的想法。至于集成测试,你也可以这样做,但要问问自己你想测试什么:是切面本身,还是Spring AOP切点匹配是否正确?

不管怎样,让我们假设你要测试的类是:

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
展开收缩
= {}"
,
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)); }; } }

看看我如何将lambda函数提取到一个辅助方法中?这有两个效果:

  • 使logAround(ProceedingJoinPoint)通知方法更易读。
  • 允许你存根化辅助方法,而不是验证是否已经完成了记录,你只需验证是否对于Mono结果类型调用了辅助方法(对于其他结果类型不调用)。

最简单形式的测试可能如下所示:

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, _)
}
}

请注意,我如何使用Spy(即基于原始对象的部分模拟)来有选择地存根辅助方法。


更新: 进行更综合性测试的替代方法是配置日志框架,将日志记录到你可以控制和验证的目标中,例如,将日志记录到内存数据库或缓冲区,你可以访问它。

英文:

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.

huangapple
  • 本文由 发表于 2020年10月2日 06:43:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/64164101.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定