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

pcrecxhr  于 2023-11-16  发布在  其他
关注(0)|答案(1)|浏览(165)

我试图用一个包含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
}


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

  class TestableExampleClass extends ExampleClass {
    def component

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

    @Override
    getComponent() {
      return component
    } 
  }
}


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

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


有两点令我困惑:
1.当我试图将捕获的参数args[0]转换为ExampleClassTestableExampleClass时,它抛出了一个GroovyCastException。我相信这是因为它需要Component$Lambda,但我不确定如何转换。
1.使用args[0].args$2访问data属性,似乎不是一种简单的方法。这可能与上面提到的强制转换问题有关。但是,是否有更好的方法来实现这一点,例如使用args[0].data
即使不能给出直接的答案,指向一些文档或文章的指针也会很有帮助。我的搜索结果分别讨论了Groovy闭包和Java lambdas比较,但没有讨论在闭包中使用lambdas。

tkclm6bt

tkclm6bt1#

为什么你不应该做你正在尝试的事情
这种入侵式的测试简直是一场噩梦!很抱歉我的措辞过于强硬,但我想明确一点,你不应该像这样过度指定测试,Assertlambda表达式的私有final字段。为什么lambda表达式中的内容很重要呢?只需验证结果。为了进行这样的验证,你需要
1.需要了解Java中如何实现Java的内部原理,
1.这些实现细节必须在Java版本中保持不变,
1.这些实现甚至必须在JVM类型(如Oracle Hotspot,OpenJ 9等)之间保持相同。
否则,你的测试很快就会失败。你为什么要关心一个方法内部如何计算它的结果呢?一个方法应该像一个黑盒一样被测试,只有在极少数情况下你才应该使用交互测试,在这种情况下,它绝对是至关重要的,以确保对象之间的某些交互以某种方式发生(例如,为了验证一个JavaScript订阅设计模式)。

无论如何你都能做到(不要!)

说了这么多,假设这样测试确实有意义(其实并没有!),一个提示:除了访问字段args$2,你还可以访问索引为1的声明字段。当然,按名称搜索也是可能的。无论如何,你必须反思lambda的类,获取你感兴趣的声明字段,使它们可访问(记住,它们是private final),然后对它们各自的内容进行Assert。您也可以按字段类型进行过滤,以便对它们的顺序不那么敏感(此处未显示)。
此外,我不明白为什么你创建一个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()
    }

字符串
或者,为了避免显式的assert而支持Spock风格的条件:

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'
  }

相关问题