2

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

8
  • 1
    Pls share your PaymentService as well Commented May 16 at 10:37
  • @GeorgiiLvov I can't post the whole code, but I added a simplified version. Again, there is no issue when testing the code manually with curl. Commented May 17 at 17:26
  • okay, could you add the org.springframework.transaction.annotation.Transactional annotation above the createPayment method and run the test Commented May 17 at 18:42
  • Same thing, the fields are still null. Commented May 17 at 18:57
  • pls share also your full test class Commented May 17 at 19:10

1 Answer 1

2

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
    }
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, the @JsonProperty(access = JsonProperty.Access.READ_ONLY) is indeed the issue. Unfortunately I don't yet have enough reputation to upvote your answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.