Search code examples
javaspringmavenresttemplatespock

Stubbing when initializing fails when stub returns stub


I have the following Spring code to test with Spock:

@Service
@RequiredArgsConstructor
public class MyService {
  private final RestTemplateBuilder restTemplateBuilder;
  // ...

  public Path downloadFile(String url) {
    try {
      ResponseEntity<byte[]> response = buildRestTemplate().getForEntity(url, byte[].class);
      File tempFileZip = File.createTempFile("myTempFile", ".zip");
      FileUtils.writeByteArrayToFile(tempFileZip, response.getBody());
      return tempFileZip.toPath();
    } catch (Exception e) {
      // ...
    }
  }

  private RestTemplate buildRestTemplate() {
    SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    Proxy proxy = new Proxy(/*...*/);
    requestFactory.setProxy(proxy);
    return restTemplateBuilder.requestFactory(() -> requestFactory).build();
  }
}

In this code a file is downloaded. I have a sample file in my test/resources folder, which I want to give to the result of a stubed download process. This is one way I can get it to work and I have some questions on this:

package de.scrum_master.stackoverflow.q59630155

class MySpec extends Specification {

  RestTemplate restTemplate = Stub {
    getForEntity(_, byte[].class) >>
      ResponseEntity.ok(
        Files.readAllBytes(
          Paths.get(
            getClass()
              .getResource("/data/MySample/MySample.zip")
              .toURI()
          )
        )
      )
  }
  RestTemplateBuilder restTemplateBuilder = Stub()
  def myService = new MyService(restTemplateBuilder)

  def setup() {
    with(restTemplateBuilder) {
      requestFactory(_) >> restTemplateBuilder
      build() >> restTemplate
    }
  }

  def "downloadFile"() {
    when:
    def response = myService.downloadFile()

    then:
    // ...

    cleanup:
    Files.delete(response)
  }
}

A) I'd like to better understand why the path of the resource file evaluates through debugger to ...target/test-classes/data/ebaRegistrySample/ebaRegistrySample.zip instead of something containing /test/resources Is it because of maven?

EDIT: It has been answered in the comments that it's because of maven inded.

B) Stubbing RestTemplateBuilder in setup() or in the test works fine. Also stubbing RestTemplate in there or when initializing in spec as shown works as well. But when I also stub RestTemplateBuilder at initialization in the spec:

package de.scrum_master.stackoverflow.q59630155

class MySpec extends Specification {
  // ...

  RestTemplateBuilder restTemplateBuilder = Stub {
      requestFactory(_) >> restTemplateBuilder
      build() >> restTemplate
  }

  // ...
}

I get a

java.lang.NullPointerException: null

I get the same exception when I do

build() >> null

I suspect that stubbing both a restTemplate and restTemplateBuilder at initialization in Spec results in the restTamplate not yet having a value when restTemplateBuilder tries to access it. Chaining stubs like this at initilization seems that should work I think, so it could be a bug.

Is it a bug or is there another reason why it doesn't work?

EDIT: It does work when I stub the requestFactory method seperately after initilization. So it's just a different error of the C case.

C) Stubbing RestTemplateBuilder at its initialization doesn't work in general. While the previous case results in an exception, placing it in setup() or in the test:

package de.scrum_master.stackoverflow.q59630155

class MySpec extends Specification {

  def "downloadFile"() {
    setup:
    RestTemplate restTemplate = Stub {
    getForEntity(_, byte[].class) >>
      ResponseEntity.ok(
        Files.readAllBytes(
          Paths.get(
            getClass()
              .getResource("/data/MySample/MySample.zip")
              .toURI()
          )
        )
      )
    }
    RestTemplateBuilder restTemplateBuilder = Stub() {
      requestFactory(_) >> restTemplateBuilder
      build() >> restTemplate
    }
    def myService = new MyService(restTemplateBuilder)

    // ...
  }
}

actually results in compilation error:

requestFactory(_) >> restTemplateBuilder
No candidates found for method call restTemplateBuilder

EDIT: A coworker suggests this is actually intended, because it protects from an indefinite recursive memory allocation in the stack.

Is it a bug or is there another reason why it doesn't work?


Solution

  • Look at your own code again:

    RestTemplateBuilder restTemplateBuilder = Stub {
      requestFactory(_) >> restTemplateBuilder
      build() >> restTemplate
    }
    

    You are trying to stub a method returning restTemplateBuilder before the restTemplateBuilder stub object has been created, it is a self-reference, similar to writing String text = "xy" + text. This also explains why it works in two steps: first creating the stub instance and then stubbing a method referencing the previously created object.

    There is no bug. The problem sits in front of the keyboard. No offense meant, this happened to me before, too. ;-)