【问题标题】:Spock -Unit Test:How to write spock unit test for @around annotation which takes MonoSpock -Unit Test:如何为采用 Mono 的 @around 注释编写 spock 单元测试
【发布时间】:2020-10-01 22:43:57
【问题描述】:

您好,我正在使用以下代码在我的 webflux 应用程序中使用 aop 打印日志,我在编写单元/集成测试时遇到问题?我们可以在这里验证日志交互吗?任何帮助将不胜感激

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

测试失败。 不知何故,当我调试指针没有进入 doOnNext 方法时。而且我不确定如何在上面的 Logging 方面断言日志交互。在 Junit5 中,我知道我可以为每个方法使用 mockito 并返回一些东西,但是我如何在 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") }

  }
}

是否建议为 @Loggable 注释编写集成测试,因为它只是记录不知道如何编写断言日志语句的集成测试

【问题讨论】:

  • 能否请您展示您目前进行的测试并解释您遇到的问题?
  • 将我的测试添加到问题中。
  • 被测代码仍然缺失,名称LoggerAspect与测试中使用的LogAspect不一样。在方面的建议方法的末尾也缺少return result; 语句。如果结果不是Mono,您还必须返回一些内容。我认为在将代码发布到此处之前实际编译和运行代码总是一个好主意,因为自愿帮助您的人不想在重现实际问题之前首先弄清楚如何在结构上修复您的代码。
  • 此外,没有导入和包名称,但我假设您的 @Slf4j 注释来自 Lombok。 Lombok 创建的内容类似于private static final Logger log = LoggerFactory.getLogger(LogAspect.class);,因此您不能为该成员分配任何内容,因为它既是最终的又是私有的。此外,没有构造函数 LogAspect(Logger logger) 像您在测试中伪造使用它。所以这甚至不能编译。
  • 我偶然发现了this article。也许您应该将其作为来源提及,而不是仅仅复制代码,假装它是您自己的。

标签: java spring-webflux spring-aop spock


【解决方案1】:

就像我在评论中所说,如果不使用 PowerMock 或类似工具等附加工具,您将无法轻松模拟 private static final 字段。我认为每当你需要这样的东西时,你应该重构你的代码以获得更好的可测试性。这是一个远非完美的想法,但我想给你一个关于如何对你的方面进行单元测试的想法。至于集成测试,你也可以这样做,但问问自己你想测试什么:方面或 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[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));
    };
  }
}

看看我是如何将 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(即基于原始对象的部分模拟)来选择性地存根辅助方法。


更新:更多集成测试的替代方法是配置您的日志框架以登录到您可以控制和验证的目标,例如登录到内存数据库或您可以访问的缓冲区。

【讨论】:

  • 非常感谢简单的解释和干净高效的测试
  • 但是,当我运行测试时,出现以下错误。无法为类 com.optum.obapi.plans.common.log.LogAspect 创建模拟,因为 Java 模拟无法模拟最终类。如果被测代码是用 Groovy 编写的,请使用 Groovy 模拟。
  • 我们如何在 kotlin 中编写相同的程序?对不起,我的实现是 kotlin?当我使用 kotlin 时,如果我使用真正的类而不是 Spy(),我遇到了与早期指针不调用 getConsumer 方法相同的问题我遇到了错误
  • NVM 我缺少@Component 注释
  • 您的新问题与原始问题无关,也与我的回答无关。也许你想问一个新问题。您还想阅读有关 Groovy 模拟的 Spock 手册部分。如果与非 Groovy 类一起使用,它们的行为就像简单的 Spock 模拟,因此得名。我的解决方案也应该适用于在 Kotlin 中实现的方面和应用程序。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
相关资源
最近更新 更多