Search code examples
javagroovylambdaclosuresspock

Access Lambda Arguments with Groovy and Spock Argument Capture


I am trying to unit test a Java class with a method containing a lambda function. I am using Groovy and Spock for the test. For proprietary reasons I can't show the original code.

The Java method looks like this:

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

Where component#doCall has the following signature:

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

The groovy test looks like this:

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

The captured argument, args, shows up as follows in the debug window if I place a breakpoint on the assert line:

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

There are two points confusing me:

  1. When I try to cast the captured argument args[0] as either ExampleClass or TestableExampleClass it throws a GroovyCastException. I believe this is because it is expecting Component$Lambda, but I am not sure how to cast this.

  2. Accessing the data property using args[0].args$2, doesn't seem like a clean way to do it. This is likely linked to the casting issue mentioned above. But is there a better way to do this, such as with args[0].data?

Even if direct answers can't be given, a pointer to some documentation or article would be helpful. My search results discussed Groovy closures and Java lambdas comparisons separately, but not about using lambdas in closures.


Solution

  • Why you should not do what you are trying

    This invasive kind of testing is a nightmare! Sorry for my strong wording, but I want to make it clear that you should not over-specify tests like this, asserting on private final fields of lambda expressions. Why would it even be important what goes into the lambda? Simply verify the result. In order to do a verification like this, you

    1. need to know internals of how lambdas are implemented in Java,
    2. those implementation details have to stay unchanged across Java versions and
    3. the implementations even have to be the same across JVM types like Oracle Hotspot, OpenJ9 etc.

    Otherwise, your tests break quickly. And why would you care how a method internally computes its result? A method should be tested like a black box, only in rare cases should you use interaction testing, where it is absolutely crucial in order to make sure that certain interactions between objects occur in a certain way (e.g. in order to verify a publish-subscribe design pattern).

    How you can do it anyway (dont!!!)

    Having said all that, just assuming for a minute that it does actually make sense to test like that (which it really does not!), a hint: Instead of accessing the field args$2, you can also access the declared field with index 1. Accessing by name is also possible, of course. Anyway, you have to reflect on the lambda's class, get the declared field(s) you are interested in, make them accessible (remember, they are private final) and then assert on their respective contents. You could also filter by field type in order to be less sensitive to their order (not shown here).

    Besides, I do not understand why you create a TestableExampleClass instead of using the original.

    In this example, I am using explicit types instead of just def in order to make it easier to understand what the code does:

        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()
        }
    

    Or, in order to avoid the explicit assert in favour of a Spock-style condition:

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