spring-bootspring-mvcspring-rest

How to write Spock Test for RestClient that returns a RequestBodyUriSpec reponse


I am attempting to write a Spock test for testing a class which uses a Spring RestClient using a fluent api. However, I am unable to correctly mock all the parts and I am getting a NullPointerException. The code is very simple, it uses an injected RestClient like this:

 package com.scf.client;

 import com.scf.domain.model.ProductSearchRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestClient;
 import com.scf.domain.model.ProductCatalog;

 import static org.springframework.http.MediaType.APPLICATION_JSON;

 @Slf4j
 @Component
 @RequiredArgsConstructor
 public class ProductRestClient {
    private final RestClient restClient;

    public ResponseEntity<ProductCatalog> findProduct(ProductSearchRequest request){
       return  restClient.post()
            .uri("/products/catalog")
            .contentType(APPLICATION_JSON)
            .body(request)
            .retrieve()
            .toEntity(ProductCatalog.class);
      }
 }

Here are the domain classes

package com.scf.domain.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

 @Data
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
 public class ProductCatalog {
     String productId;
     String productName;
     String productDescription;
 }
package com.scf.domain.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductSearchRequest {
     String catalogId;
     Boolean inStock;
}

Here is my attempt to write a test for the above:

package com.scf.client

import com.scf.domain.model.ProductCatalog
import com.scf.domain.model.ProductSearchRequest
import org.springframework.web.client.RestClient
import spock.lang.Specification

class ProductRestClientSpec extends Specification {

    RestClient restClient = Mock()
    ProductRestClient productRestClient
    RestClient.RequestBodyUriSpec mockRequestBodyUriSpec = Mock(RestClient.RequestBodyUriSpec.class);

    def setup() {
        productRestClient = new ProductRestClient(restClient)
    }

    def "Should retrieve a Product Catalog for a valid request"() {
        given:
        def request = createProductSearchRequest()
        def response = createProductCatalogResponse()

        when:
        def result = productRestClient.findProduct(request)

        then:
        1 * restClient./post|uri|contentType|body|retrieve|onStatus|toEntity/(*_) >> mockRequestBodyUriSpec
        1 * mockRequestBodyUriSpec.get(_) >> { args -> response }
    }

    def createProductSearchRequest() {
        ProductSearchRequest.builder()
                .catalogId("XF-3333")
                .inStock(true)
                .build()
    }

    def createProductCatalogResponse() {
        ProductCatalog.builder()
                .productDescription("Ralph Lauren Fragrance")
                .productId("PRD-99W222")
                .productName("Ralph Cologne")
                .build()

    }
}

Unfortunately, this does not work. I get a null pointer exception. I want to be able to test getting the ProductCatalog from the ResponseEntity. How do I mock the RequestBodyUriSpec returned? I've been struggling with this for a day and a half, please help


Solution

  • Quoting my own comment:

    In the call chain restClient.post().uri("/products/catalog").contentType(APPLICATION_JSON).body(request).retrieve().toEntity(ProductCatalog.class), the return types of each method are different, which means that you would have to mock and stub several more classes instead of always making your restClient return mockRequestBodyUriSpec.

    How about something like this?

    package de.scrum_master.stackoverflow.q79510107
    
    import org.springframework.http.HttpStatus
    import org.springframework.http.ResponseEntity
    import org.springframework.web.client.RestClient
    import spock.lang.Specification
    
    class ProductRestClientSpec extends Specification {
      RestClient.ResponseSpec responseSpec = Mock()
      RestClient.RequestBodyUriSpec requestBodyUriSpec = Stub() {
        /uri|contentType|body/(_) >> Stub(RestClient.RequestBodySpec) {
          retrieve() >> responseSpec
        }
      }
      RestClient restClient = Mock()
      ProductRestClient productRestClient = new ProductRestClient(restClient)
    
      def "Should retrieve a Product Catalog for a valid request"() {
        given:
        def request = createProductSearchRequest()
        def response = createProductCatalogResponse()
    
        when:
        def result = productRestClient.findProduct(request)
    
        then:
        1 * restClient.post() >> requestBodyUriSpec
        1 * responseSpec.toEntity(_) >> new ResponseEntity<ProductCatalog>(response, HttpStatus.OK)
        result.body == response
      }
    
      def createProductSearchRequest() {
        ProductSearchRequest.builder()
          .catalogId("XF-3333")
          .inStock(true)
          .build()
      }
    
      def createProductCatalogResponse() {
        ProductCatalog.builder()
          .productDescription("Ralph Lauren Fragrance")
          .productId("PRD-99W222")
          .productName("Ralph Cologne")
          .build()
      }
    
    }
    

    Or if you can do without over-specifying the test, checking which methods are called how often, simplify to:

    package de.scrum_master.stackoverflow.q79510107
    
    import org.springframework.http.HttpStatus
    import org.springframework.http.ResponseEntity
    import org.springframework.web.client.RestClient
    import spock.lang.Specification
    
    class ProductRestClientSpec extends Specification {
      RestClient.ResponseSpec responseSpec = Mock()
      RestClient restClient = Stub() {
        post() >> Stub(RestClient.RequestBodyUriSpec) {
          /uri|contentType|body/(_) >> Stub(RestClient.RequestBodySpec) {
            retrieve() >> responseSpec
          }
        }
      }
      ProductRestClient productRestClient = new ProductRestClient(restClient)
    
      def "Should retrieve a Product Catalog for a valid request"() {
        given:
        def request = createProductSearchRequest()
        def response = createProductCatalogResponse()
    
        and:
        responseSpec.toEntity(_) >> new ResponseEntity<ProductCatalog>(response, HttpStatus.OK)
    
        expect:
        productRestClient.findProduct(request).body == response
      }
    
      def createProductSearchRequest() {
        ProductSearchRequest.builder()
          .catalogId("XF-3333")
          .inStock(true)
          .build()
      }
    
      def createProductCatalogResponse() {
        ProductCatalog.builder()
          .productDescription("Ralph Lauren Fragrance")
          .productId("PRD-99W222")
          .productName("Ralph Cologne")
          .build()
      }
    
    }
    

    Try it in the Groovy Web Console. As you can see there, it is a complete, minimal reproducer which does not even need Lombok, because the helper classes referenced by your application and test code are implemented using Groovy annotations like @Canonical and @Builder. It also uses @Grab(group='org.springframework', module='spring-web', version='6.2.5'), so the application code can really use Spring and we are not testing dummy classes.