javaspring-boottestingjunitresttemplate

When performing integration tests for Springboot app with RestTemplate, why are not all returned values set?


I have a Springboot API that includes a model class Payment, a @Service class PaymentService, as well as a JPA repository, controller, and some utility classes. I have integration tests that mostly work using junit, h2 in-memory db, and RestTemplate. When I run the test to, for example, create a new payment, the @Service class runs, does the business logic, saves the new object in the in-memory database, sets a few more fields, and then returns the created object.

My test:

@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)                                                                                                                                                                                      
@FieldDefaults(level=AccessLevel.PRIVATE)                                                                                                                                                                                                                      
public class PaymentControllerCreateTest {                                                                                                                                                                                                                     
                                                                                                                                                                                                                                                               
        @LocalServerPort                                                                                                                                                                                                                                       
        int port;                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                               
        String baseUrl = "http://localhost";                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                               
        static RestTemplate restTemplate = null;                                                                                                                                                                                                               
                                                                                                                                                                                                                                                               
        @BeforeAll                                                                                                                                                                                                                                             
        public static void init() {                                                                                                                                                                                                                            
                restTemplate = new RestTemplate();                                                                                                                                                                                                             
                restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());                                                                                                                                                            
        }                                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                                               
        @BeforeEach                                                                                                                                                                                                                                            
        public void setup() {                                                                                                                                                                                                                                  
                baseUrl += ":" + port + "/payment";                                                                                                                                                                                                            
        }                                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                                               
        @Test                                                                                                                                                                                                                                                  
        @Sql(statements="delete from payment_line_items", executionPhase=Sql.ExecutionPhase.AFTER_TEST_METHOD)                                                                                                                                                 
        @Sql(statements="delete from payment", executionPhase=Sql.ExecutionPhase.AFTER_TEST_METHOD)                                                                                                                                                            
        public void testCreateBasicPayment() throws Exception {                                                                                                                                                                                                
            headers.setContentType(MediaType.APPLICATION_JSON);                                         
            headers.set("Authorization", "token");                                                      
            HttpEntity<Payment> entity = new HttpEntity<>(payment, headers);                            
            ResponseEntity<Payment> response = restTemplate.postForEntity(baseUrl, entity, Payment.class);                                                                                                          
            assert response.getStatusCode().is2xxSuccessful();                                          
            Payment created = response.getBody(); 
    
            // these assertions pass, but amount and currency code are set in the POSTed object                                                      
            assert created.getCardInfo() == null;                                                       
            assert created.getAmount().compareTo(payment.getAmount()) == 0;                             
            assert created.getCurrencyCode().equals(payment.getCurrencyCode());                         
    
            // these assertions fail, and the fields are either autogenerated or set in the service
            assert created.getId() != null;
            assert created.getTransId() != null;                                                                                                                                                                                                            
        }
}

Relevant bits of Payment class:

@Entity @Table(name="payment")                                                                              
@JsonInclude(Include.NON_NULL)                                                                              
@Data                                                                                                       
@FieldDefaults(level=AccessLevel.PRIVATE)                                                                   
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})                                              
public class Payment {

        @Id                                                                                                 
        @GeneratedValue                                                                                     
        @JsonProperty(access=Access.READ_ONLY)                                                                                                                                                                 
        UUID id;

        @JsonProperty(access=Access.READ_ONLY)                                                              
        String transId;

        @NotNull
        @JsonSerialize(using=BigDecimalSerializer.class)
        BigDecimal amount;

        @NotNull
        String currencyCode;

    ...

}

Simplified @Service class:

@Service                                                                                                   
@NoArgsConstructor                                                                                         
public class PaymentService {                                                                              
                                                                                                                                              
        @Autowired                                                                                         
        PaymentRepository repository;                                                                                                                                                                                                    
                                                                                                           
        public Payment createPayment(Payment payment) {                                                                                                     
                // do business logic, submit payment, etc.
                payment.setTransId(tid);
                payment.setCardInfo(null); // set payment info to null so it isn't included in response          
                                                               
                return repository.saveAndFlush(payment);
        }
}

I can verify that the code in the @Service class is running (via print statements and other ways related to the specific business logic). However, the returned object from restTemplate.postForEntity() is missing some fields, namely all those that are set by the @Service code and @GeneratedValue fields. Everything works as expected when running manual tests using curl. How can I get the correct object to be returned from the restTemplate.postForEntity()?

MRE for the issue: https://github.com/honreir/SO-79624582-mre


Solution

  • The issue is the use of @JsonProperty(access = JsonProperty.Access.READ_ONLY) on the id and transId fields in your Payment entity. This instructs Jackson to ignore these fields during deserialization, which includes deserializing the HTTP response body (response.getBody()) in your integration test using RestTemplate.

    To resolve this properly and avoid mixing persistence and API concerns, you should introduce DTOs in your code, like this:

    @RestController
    @RequiredArgsConstructor
    public class PaymentController {
        
        private final PaymentService service;
    
        @PostMapping(value = "/payment", 
                consumes = {"application/json"}, 
                produces = {"application/json"})
        ResponseEntity<?> createPayment(@RequestBody @Valid PaymentRequest paymentRequest) throws Exception {
            PaymentResponse paymentResponse = service.createPayment(paymentRequest);
            return new ResponseEntity<>(paymentResponse, HttpStatus.CREATED);
        }
    }
    

    where:

    // Excludes id and transId to prevent the client from sending them
    public record PaymentRequest(@NotNull
                                 String currencyCode,
                                 @NotNull
                                 @JsonSerialize(using = BigDecimalSerializer.class)
                                 BigDecimal amount,
                                 CardInfo cardInfo) {
    }
    
    // Excludes cardInfo
    public record PaymentResponse(UUID id,
                             String transId,
                             String currencyCode,
                             BigDecimal amount,
                             Instant timestamp ) {
    }
    

    You also need to create and use in your service a PaymentMapper to map PaymentRequest to Payment entity and to map Payment entity to PaymentResponse.

    Alternative, if refactoring to use DTOs isn’t feasible at the moment, another approach is to assert on the raw JSON returned:

        @Test
        @Sql(statements="delete from payment", executionPhase=Sql.ExecutionPhase.AFTER_TEST_METHOD)
        void testCreateBasicPayment() throws Exception {
            Payment payment = generatePayment();
            HttpHeaders headers = generateHttpHeaders();
    
            HttpEntity<Payment> entity = new HttpEntity<>(payment, headers);
    
            ResponseEntity<String> response = restTemplate.postForEntity(baseUrl, entity, String.class);
    
            assert response.getStatusCode().is2xxSuccessful();
    
            /*
             *{"id":"c4fb085f-73be-488e-8ba9-bd492f59b4cb","timestamp":"2025-05-18T10:40:02.706665Z",
             * "transId":"some ID","amount":1024.00,"currencyCode":"USD"}
             */
            String responseBodyJson = response.getBody();
    
            // assert json here like simple string or using some libraries like net.javacrumbs.json-unit assertj
        }