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
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 yourrestClient
returnmockRequestBodyUriSpec
.
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.