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
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
}