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

Access Lambda Arguments with Groovy and Spock Argument Capture

我正在尝试使用包含 lambda 函数的方法对 Java class 进行单元测试。我正在使用 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
}

groovy 测试如下所示:

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.asUtf8String() == 'teststring'
      return new CompletableFuture()   
    }
  }

  class TestableExampleClass extends ExampleClass {
    def component

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

    @Override
    getComponent() {
      return component
    } 
  }
}

如果我在 assert 行上放置断点,则捕获的参数 args 在调试 window 中显示如下:

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

有两点让我很困惑:

  1. 当我尝试将捕获的参数 args[0] 转换为 ExampleClassTestableExampleClass 时,它会抛出 GroovyCastException。我相信这是因为它期待 Component$Lambda,但我不确定如何投射它。

  2. 使用 args[0].args 访问 data 属性 似乎不是一种干净的方法。这可能与上述铸造问题有关。但是有没有更好的方法来做到这一点,比如 args[0].data?

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

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

这种侵入式测试是一场噩梦!抱歉我措辞强硬,但我想明确表示,您不应该 over-specify 像这样测试,在 lambda 表达式的私有最终字段上断言。为什么进入 lambda 的内容如此重要?只需验证结果。为了进行这样的验证,您

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

否则,您的测试很快就会失败。为什么你会关心一个方法如何在内部计算它的结果?一个方法应该像黑盒子一样被测试,只有在极少数情况下你才应该使用交互测试,在这种情况下,为了确保对象之间的某些交互以某种方式发生(例如为了验证 publish-subscribe 设计模式)。

你怎么能做到呢(不要!!!)

说了那么多,暂时假设这样的测试确实有意义(实际上没有意义!),提示:除了访问字段 args,您可以也可以访问索引为 1 的已声明字段。当然,也可以按名称访问。无论如何,您必须反思 lambda 的 class,获取您感兴趣的声明字段,使它们可访问(记住,它们是 private 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')
      nameField.accessible = true
      byte[] nameBytes = nameField.get(requestConsumer)
      assert new String(nameBytes, Charset.forName("UTF-8")) == 'teststring'
      return new CompletableFuture()
    }

或者,为了避免显式 assert 支持 Spock-style 条件:

  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')
      nameField.accessible = true
      byte[] nameBytes = nameField.get(requestConsumer)
      name = new String(nameBytes, Charset.forName("UTF-8"))
      return new CompletableFuture()
    }
    name == 'teststring'
  }