使用 Groovy 和 Spock 参数捕获访问 Lambda 参数

Access Lambda Arguments with Groovy and Spock Argument Capture

提问人:q-q 提问时间:2/12/2022 更新时间:10/17/2023 访问量:916

问:

我正在尝试使用包含 lambda 函数的方法对 Java 类进行单元测试。我正在使用 Groovy 和 Spock 进行测试。由于专有原因,我无法显示原始代码。

Java 方法如下所示:

class ExampleClass {
  AsyncHandler asynHandler;
  Component componet;

  Component getComponent() {
    return component;
  }

  void exampleMethod(String input) {
    byte[] data = input.getBytes();

    getComponent().doCall(builder -> 
      builder
        .setName(name)
        .data(data)
        .build()).whenCompleteAsync(asyncHandler);
  }
}

其中具有以下签名:component#doCall

CompletableFuture<Response> doCall(Consumer<Request> request) {
  // do some stuff
}

时髦测试如下所示:

class Spec extends Specification {
  def mockComponent = Mock(Component)

  @Subject
  def sut = new TestableExampleClass(mockComponent)

  def 'a test'() {
    when:
    sut.exampleMethod('teststring')

    then:
    1 * componentMock.doCall(_ as Consumer<Request>) >> { args ->
      assert args[0].args$2.asUtf8String() == 'teststring'
      return new CompletableFuture()   
    }
  }

  class TestableExampleClass extends ExampleClass {
    def component

    TestableExampleClass(Component component) {
      this.component = component;
    }

    @Override
    getComponent() {
      return component
    } 
  }
}

如果我在该行上放置一个断点,捕获的参数 , 将在调试窗口中显示如下:argsassert

args = {Arrays$ArrayList@1234} size = 1
  > 0 = {Component$lambda}
    > args$1 = {TestableExampleClass}
    > args$2 = {bytes[]}

有两点让我感到困惑:

  1. 当我尝试将捕获的参数转换为 either 或它抛出一个 .我相信这是因为它正在期待,但我不确定如何投射它。args[0]ExampleClassTestableExampleClassGroovyCastExceptionComponent$Lambda

  2. 使用 访问属性似乎不是一个干净的方法。这可能与上述选角问题有关。但是有没有更好的方法可以做到这一点,例如使用 ?dataargs[0].args$2args[0].data

即使无法给出直接答案,指向某些文档或文章的指针也会有所帮助。我的搜索结果分别讨论了 Groovy 闭包和 Java lambda 的比较,但没有讨论在闭包中使用 lambda。

Java Groovy Lambda 闭包 Spock

评论


答:

1赞 kriegaex 2/12/2022 #1

为什么你不应该做你正在尝试的事情

这种侵入性测试是一场噩梦!对不起,我的措辞很强硬,但我想明确指出,您不应该过度指定这样的测试,在 lambda 表达式的私有最终字段上断言。为什么 lambda 中的内容甚至很重要?只需验证结果即可。为了进行这样的验证,您

  1. 需要了解 lambda 在 Java 中是如何实现的内部结构,
  2. 这些实现细节必须在 Java 版本之间保持不变,并且
  3. 在JVM类型(如Oracle Hotspot,OpenJ9等)中,实现甚至必须相同。

否则,您的测试很快就会中断。你为什么要关心一个方法在内部如何计算它的结果?一种方法应该像黑匣子一样进行测试,只有在极少数情况下才应该使用交互测试,这对于确保对象之间的某些交互以某种方式发生(例如,为了验证发布-订阅设计模式)是绝对关键的。

无论如何你怎么能做到这一点(不要!!)

说了这么多,只是假设像这样测试确实有意义(它真的没有!),提示:除了访问字段,您还可以访问索引为 1 的声明字段。当然,也可以按名称访问。无论如何,您必须反思 lambda 的类,获取您感兴趣的声明字段,使它们可访问(记住,它们是),然后对它们各自的内容进行断言。您还可以按字段类型进行筛选,以降低对字段顺序的敏感度(此处未显示)。args$2private final

此外,我不明白你为什么要创建一个而不是使用原始版本。TestableExampleClass

在此示例中,我使用显式类型,而不仅仅是为了让更容易理解代码的作用:def

    then:
    1 * mockComponent.doCall(_ as Consumer<Request>) >> { args ->
      Consumer<Request> requestConsumer = args[0]
      Field nameField = requestConsumer.class.declaredFields[1]
//    Field nameField = requestConsumer.class.getDeclaredField('arg$2')
      nameField.accessible = true
      byte[] nameBytes = nameField.get(requestConsumer)
      assert new String(nameBytes, Charset.forName("UTF-8")) == 'teststring'
      return new CompletableFuture()
    }

或者,为了避免明确支持 Spock 式条件:assert

  def 'a test'() {
    given:
    String name

    when:
    sut.exampleMethod('teststring')

    then:
    1 * mockComponent.doCall(_ as Consumer<Request>) >> { args ->
      Consumer<Request> requestConsumer = args[0]
      Field nameField = requestConsumer.class.declaredFields[1]
//    Field nameField = requestConsumer.class.getDeclaredField('arg$2')
      nameField.accessible = true
      byte[] nameBytes = nameField.get(requestConsumer)
      name = new String(nameBytes, Charset.forName("UTF-8"))
      return new CompletableFuture()
    }
    name == 'teststring'
  }

评论

0赞 q-q 2/12/2022
我应该提到它来自第三方库,并且它的方法之一已被覆盖。因此,访问 lambda 字段似乎是验证预期行为的唯一方法。但是,我会重新考虑是否有更好的方法,而不会过度指定。你的例子已经澄清了对我有什么好处。谢谢你的回答。ExampleClassvoidargs$2
0赞 kriegaex 2/13/2022
在我们专注于如何从技术上测试某些东西之后,这总是一个坏主意,你可以打开一个新问题,展示一个最小但完整且可重复的例子,以及对你真正想要验证的内容的一些解释。那么也许我可以提出一些建议。