Verdict
No
Do not write unit tests for a DTO that only declares fields and generated accessors; check its JSON in the test of the endpoint that sends or receives it.
Why
- Blast radiususers
- Change frequencyrarely
- Detectabilityimmediately
- Reversibilitytrivial
- Test costtrivial
Do not write unit tests for a DTO that only declares fields and accessors. For the typical case, a request or response class written as a Java record or a Lombok class, Blast radius is users and Change frequency is rarely, because a wrong field reaches a client screen and a given DTO gains a field a few times a year. Detectability is immediately: the compiler rejects a removed field in the code that fills it, and the endpoint test that serializes the DTO fails in CI. Reversibility and Test cost are both trivial, because a revert leaves no bad data behind and a round-trip test takes minutes. Rule R8 gives Do not test, and validation or mapping code moves the decision to a test.
| When | Decision | Why |
|---|---|---|
| The DTO carries validation annotations, such as `@NotNull`, on the body of a write endpoint | Test: one valid object and one invalid object per rule | Detectability moves to eventually and Reversibility to with-effort: a missing rule stores bad records without an error |
| Installed mobile apps or a partner service that you do not ship read the DTO's JSON | Test minimally: one serialization test per DTO that asserts the field names | Detectability moves to eventually, Reversibility to with-effort and Test cost to moderate: only clients on old versions see a renamed field |
| The DTO carries an amount that a payment provider charges | Test mandatory: assert the exact JSON for amounts such as 0.10 and 19.99 | Blast radius rises to money, Detectability to eventually and Reversibility to costly: a wrong charge is refunded by hand |
| The response DTO is the only place that keeps a password hash or a home address out of the response | Test mandatory: assert that each excluded field is absent from the JSON | Blast radius rises to safety-or-legal, Detectability to never and Reversibility to impossible: leaked data stays leaked |
| The DTO has a hand-written method that copies fields from an entity, such as `fromEntity()` | Test: fill every source field and check every DTO field | Detectability moves to eventually: a forgotten field arrives as null and looks like missing data |
What breaks if you don't test
For a DTO without logic, nothing breaks that the compiler and the endpoint tests miss. The failures that reach users sit at the edges of the DTO: a JSON name that the serializer derives from an accessor, a validation annotation that never runs, a mapper that forgets a new field. A mobile app then shows an empty value, or a write endpoint saves an order without a delivery address, and support tickets report it days later.
What you lose if you over-test
A test per field adds a test file for every DTO. On an API with 60 DTOs, 60 files repeat each field list, and every new field needs an edit in two places. The tests never fail, because a generated accessor cannot drift from its field. They lift coverage while the JSON names stay unchecked, so a coverage gate reports the DTO package as done.
What to do instead
In the HTTP-level test of each endpoint, assert the JSON fields that the client reads, so a renamed or missing field fails CI. Generate accessors, equals and hashCode with records or Lombok so there is no hand-written body to get wrong. For a DTO that another service reads, add one test with Spring Boot's @JsonTest, as the Spring Boot guide to JSON tests shows.
When the answer changes
- The DTO gains a method with logic, such as validation or a copy from an entity.
- A client that you do not ship, such as an installed mobile app, reads the JSON field names.
- The DTO carries money amounts or personal data.
Code example + Counterexample
The boolean field that lost its prefix
This Lombok DTO has no hand-written code, and it still breaks the API:
@Data
@AllArgsConstructor
public class AccountDto {
private String email;
private boolean isPremium;
}
@JsonTest
class AccountDtoJsonTest {
@Autowired JacksonTester<AccountDto> json;
@Test
void writesTheFieldNamesTheAppReads() throws Exception {
AccountDto dto = new AccountDto("ana@example.com", true);
assertThat(json.write(dto))
.hasJsonPathBooleanValue("@.isPremium"); // fails: Jackson writes "premium"
}
}
Lombok names the getter isPremium() for a boolean field that starts with is, and Jackson derives the property name premium from that getter. A client that reads isPremium gets undefined and hides paid features from a paying customer, while the compiler and a round-trip test of the getter see nothing wrong. Detectability moves to eventually and Test cost to moderate, so the decision moves from Do not test to Test minimally. The fix is to rename the field to premium and set its JSON name with @JsonProperty("isPremium").
Related questions
FAQ
- Is it worth writing a unit test for a DTO with basic getters and setters?
No, a DTO with only basic getters and setters needs no unit test, because Lombok or the compiler generates accessors without logic. Check the DTO's JSON in the endpoint test instead, where a renamed field fails CI.
- Should POJOs be tested?
No, a POJO that only holds fields needs no tests of its own; test the code that creates and reads it. A POJO method with logic, such as a hand-written
equals, gets tests like any other method.- Should I test DTO validation annotations?
Yes, test DTO validation annotations with one valid and one invalid object per rule, using the Bean Validation
Validatorin a plain unit test. Add one endpoint test that expects a 400 response for an invalid body, because Spring runs the annotations only when the controller parameter carries@Valid.- Should I test the mapping between entities and DTOs?
Yes, test a hand-written entity-to-DTO mapper with one test that fills every source field and checks every DTO field, because a forgotten field arrives as null. With MapStruct, set
unmappedTargetPolicy = ReportingPolicy.ERRORso the build fails on a DTO field that no source fills.