Series MapLesson 27 / 32
Deepen PracticeOrdered learning track

Learn Java Data Mapper Json Xml Validation Part 027 Mapping Testing Strategy

11 min read2055 words
PrevNext
Lesson 2732 lesson track1927 Deepen Practice

title: Learn Java Data Mapper, JSON/XML Processing & Validation - Part 027 description: Mapping test strategy untuk Java mapper layer: golden samples, semantic assertions, property-based checks, mutation surface, contract fixtures, MapStruct generated code review, and production-quality mapper test architecture. series: learn-java-data-mapper-json-xml-validation seriesTitle: Learn Java Data Mapper, JSON/XML Processing & Validation order: 27 partTitle: Mapping Test Strategy: Golden Samples, Property-Based Checks, Mutation Surface, Contract Fixtures tags:

  • java
  • mapstruct
  • testing
  • data-mapper
  • contract-testing
  • golden-samples
  • property-based-testing
  • dto
  • validation
  • jackson date: 2026-06-29

Part 027 — Mapping Test Strategy: Golden Samples, Property-Based Checks, Mutation Surface, Contract Fixtures

Target skill: mampu membuktikan mapper benar secara semantic, bukan hanya compile. Ini mencakup golden sample, contract fixture, enum coverage, null/absence matrix, property-based checks, mutation surface, generated-code review, dan regression strategy.

MapStruct memberi compile-time safety. Jackson/JAXB memberi parser/binding capability. Jakarta Validation memberi constraint checking. Tetapi semua itu tidak otomatis membuktikan bahwa mapping kita benar secara bisnis.

Compile success hanya menjawab:

Apakah kode mapping bisa dihasilkan dan dikompilasi?

Test mapping menjawab:

Apakah field yang benar, dengan makna yang benar, berpindah ke target yang benar, dalam semua kondisi penting?

Mental model:

Mapper test is contract evidence. It protects meaning across refactor, schema evolution, and dependency upgrades.


1. Kaufman Deconstruction

Subskill mapping test:

SubskillKemampuan
Identify mapping riskMenentukan mapper mana perlu test ringan vs heavy
Golden sample testingMembandingkan object/output terhadap fixture yang stabil
Semantic assertionMenguji meaning, bukan hanya field equality
Null/absence matrixMenguji null, missing, empty, default, patch intent
Enum coverageMemastikan semua enum source dimapping atau ditolak sengaja
Collection testingMemastikan order, duplicates, merge semantics, empty behavior
Contract fixture testingMenggunakan JSON/XML/event fixture nyata
Property-based checksMenguji invariant mapping dengan banyak input
Mutation surface analysisMenentukan field mana paling rawan silent corruption
Generated code reviewMembaca mapper generated untuk mapping kritikal
Drift detectionMendeteksi field baru/renamed/removed
CI integrationMenjadikan mapping tests bagian dari release gate

2. Why Mapper Tests Matter

Mapping bug sering terlihat kecil tetapi dampaknya besar:

BugDampak
customerId mapped from internal DB iddata leak / wrong reference
money scale lostfinancial mismatch
enum defaulted to UNKNOWN silentlyworkflow bypass
null ignored in patchuser cannot clear field
null set in mergeaccidental data deletion
collection order changedUI/audit/export mismatch
timezone droppedregulatory timestamp error
entity graph mapped too deepdata leak / performance issue
new target field unmappedpartial response / invalid event
alias accepted forevercontract entropy

Top engineer menguji mapper bukan karena tidak percaya MapStruct, tetapi karena MapStruct tidak tahu business meaning.


3. Test Pyramid for Mapping

3.1 Mapper Unit Tests

Cepat, langsung memanggil mapper.

@Test
void mapsCustomerToResponse() {
    Customer customer = new Customer(new CustomerId("CUS-001"), "Ana");

    CustomerResponse response = mapper.toResponse(customer);

    assertThat(response.customerId()).isEqualTo("CUS-001");
    assertThat(response.fullName()).isEqualTo("Ana");
}

3.2 Boundary Fixture Tests

Baca JSON/XML fixture, deserialize, map, assert command/domain DTO.

@Test
void createPaymentFixture_mapsToCommand() throws Exception {
    CreatePaymentRequest request = objectMapper.readValue(
        fixture("create-payment-valid-card.json"),
        CreatePaymentRequest.class
    );

    CreatePaymentCommand command = mapper.toCommand(request);

    assertThat(command.money().amount()).isEqualByComparingTo("100.00");
}

3.3 Golden Contract Tests

Pastikan output event/API/export tetap stabil.

@Test
void paymentApprovedEvent_goldenJson() throws Exception {
    PaymentApprovedEvent event = mapper.toEvent(fixturePayment());

    assertThatJson(objectMapper.writeValueAsString(event)).isEqualTo(
        fixtureText("payment-approved-event-v1.json")
    );
}

3.4 Integration Tests

Lewat HTTP/event/import path asli.

JSON bytes -> Jackson -> Validation -> MapStruct -> Use Case

Ini memastikan mapper yang diuji sama dengan runtime path.


4. Classify Mapper Risk

Tidak semua mapper butuh tingkat test sama.

Mapper TypeRiskTest Depth
simple same-name DTO mapperlowsmoke test + compile strict policy
public API response mappermedium/highgolden response tests
command request mapperhighfixture + validation + semantic assertions
event producer mapperhighgolden event + compatibility tests
event consumer mapperhighold/new/unknown fixtures
payment/money mappervery highedge cases + round-trip + property checks
patch/update mappervery highmissing/null/value matrix
provider integration mapperhighprovider fixtures + tolerant/strict policy tests
persistence entity mappermedium/highinvariant + no leak + collection tests
security/redaction mappervery highnegative tests proving secret absent

Rule:

The more a mapper crosses a trust boundary or money/state boundary, the more it deserves explicit tests.


5. Test Data Strategy

Avoid random object mothers that hide meaning.

Bad:

Customer customer = CustomerFixture.randomCustomer();

Better:

Customer customer = CustomerFixtures.activeCustomer(
    "CUS-001",
    "Ana Maria",
    "ana@example.com"
);

Best for contracts: named fixtures.

fixtures/customer/customer-active.json
fixtures/payment/create-payment-card-valid.json
fixtures/payment/create-payment-invalid-money-scale.json
fixtures/event/payment-approved-v1.json
fixtures/event/payment-approved-v1-extra-field.json
fixtures/xml/case-report-valid.xml
fixtures/xml/case-report-wrong-namespace.xml

Named fixtures become documentation.


6. Golden Sample Testing

Golden sample is a fixed expected representation.

Use for:

  • API response JSON
  • event payload JSON
  • XML export
  • CSV/JSON import output
  • audit event
  • schema evolution
  • migration testing

Example:

@Test
void customerResponse_golden() throws Exception {
    CustomerResponse response = mapper.toResponse(fixtureCustomer());

    String actual = objectMapper.writeValueAsString(response);

    assertThatJson(actual).isEqualTo("""
    {
      "customerId": "CUS-001",
      "fullName": "Ana Maria",
      "status": "ACTIVE"
    }
    """);
}

Golden tests should be reviewed like contract changes. If golden output changes, ask:

  • is this intended?
  • is it backward-compatible?
  • does consumer know?
  • does schema version need bump?
  • does migration need alias/upcaster?

7. Semantic Assertions

Do not only assert object equality if target equality is too broad or too weak.

Bad:

assertThat(response).isNotNull();

Weak:

assertThat(response.fullName()).isEqualTo(customer.fullName());

Better:

assertThat(response.customerId())
    .as("public customer id must use business id, not database id")
    .isEqualTo("CUS-001");

assertThat(response).doesNotHaveFieldOrProperty("passwordHash");

Semantic assertions document engineering intent.


8. Null / Absence / Empty Matrix Tests

For each boundary field, decide expected behavior.

Input StateExampleExpected
missing{}reject / unchanged / default
explicit null{ "email": null }reject / clear / unchanged
empty string{ "email": "" }reject / normalize / accept
blank string{ "email": " " }reject / trim / accept
empty array{ "tags": [] }clear / reject / empty
null array{ "tags": null }reject / clear / unchanged
absent array{}unchanged / empty / reject

Example patch test:

@Test
void patch_nullPhone_clearsPhone() throws Exception {
    CustomerEntity target = existingCustomerWithPhone("0812");
    CustomerPatchRequest patch = patchParser.parse("""
    { "phoneNumber": null }
    """);

    patchApplier.apply(patch, target);

    assertThat(target.getPhoneNumber()).isNull();
}

Example merge test:

@Test
void merge_nullPhone_keepsExistingPhone() {
    CustomerEntity target = existingCustomerWithPhone("0812");
    CustomerMergeRequest request = new CustomerMergeRequest(null, null, null);

    mapper.merge(request, target);

    assertThat(target.getPhoneNumber()).isEqualTo("0812");
}

9. Enum Coverage Tests

For internal closed enums, every source value should be mapped.

@Test
void allProviderStatusesAreMapped() {
    for (ProviderStatus status : ProviderStatus.values()) {
        assertThatCode(() -> mapper.toDomain(status))
            .as("status %s must be handled", status)
            .doesNotThrowAnyException();
    }
}

For target expectations:

@ParameterizedTest
@CsvSource({
    "WAITING,PENDING",
    "PAID,COMPLETED",
    "FAIL,FAILED"
})
void mapsProviderStatus(ProviderStatus source, PaymentStatus expected) {
    assertThat(mapper.toDomain(source)).isEqualTo(expected);
}

For external string codes:

@Test
void unknownProviderStatus_preservesRaw() {
    ProviderStatusValue status = mapper.toStatus("NEW_PROVIDER_CODE");

    assertThat(status.raw()).isEqualTo("NEW_PROVIDER_CODE");
    assertThat(status.known()).isEqualTo(ProviderStatusValue.Known.UNRECOGNIZED);
}

Do not casually collapse unknown external enum to null.


10. Money and Decimal Tests

Money mapping deserves dedicated tests.

Cases:

InputExpected
"100.00"amount 100.00, scale policy preserved
"100"accepted/rejected based on contract
"100.001"rejected if scale > 2
"0.00"accepted/rejected based on rule
negativeaccepted for reversal or rejected
JSON numberaccepted/rejected based on contract
scientific notationaccepted/rejected based on contract
currency lowercasenormalize/reject
unsupported currencyreject

Example:

@Test
void mapsMoney_preservesDecimalStringMeaning() {
    MoneyDto dto = new MoneyDto("100.00", "IDR");

    Money money = mapper.toMoney(dto);

    assertThat(money.amount()).isEqualByComparingTo("100.00");
    assertThat(money.currency()).isEqualTo(Currency.getInstance("IDR"));
}

Scale check:

@Test
void money_rejectsTooManyFractionDigits() {
    MoneyDto dto = new MoneyDto("100.001", "IDR");

    assertThatThrownBy(() -> mapper.toMoney(dto))
        .isInstanceOf(IllegalArgumentException.class);
}

11. Date-Time Tests

Test semantic time choice:

FieldExpected type
event occurrenceInstant
provider timestamp with offsetOffsetDateTime or Instant plus raw if needed
business dateLocalDate
billing monthYearMonth

Example:

@Test
void mapsOccurredAtToInstant() {
    EventDto dto = new EventDto("2026-06-29T03:00:00Z");

    EventCommand command = mapper.toCommand(dto);

    assertThat(command.occurredAt()).isEqualTo(Instant.parse("2026-06-29T03:00:00Z"));
}

Reject ambiguous timestamp:

@Test
void rejectsTimestampWithoutOffsetWhenInstantRequired() {
    EventDto dto = new EventDto("2026-06-29T10:00:00");

    assertThatThrownBy(() -> mapper.toCommand(dto))
        .isInstanceOf(DateTimeParseException.class);
}

12. Collection Mapping Tests

Collections need tests for:

  • order
  • duplicates
  • null collection
  • empty collection
  • null item
  • item conversion
  • target mutability
  • replace vs merge semantics

Example order preservation:

@Test
void mapsOrderLinesInSameOrder() {
    Order order = fixtureOrderWithLines("SKU-A", "SKU-B", "SKU-C");

    OrderResponse response = mapper.toResponse(order);

    assertThat(response.lines())
        .extracting(OrderLineResponse::sku)
        .containsExactly("SKU-A", "SKU-B", "SKU-C");
}

Example empty:

@Test
void mapsEmptyLinesToEmptyList() {
    Order order = fixtureOrderWithLines();

    OrderResponse response = mapper.toResponse(order);

    assertThat(response.lines()).isEmpty();
}

If null collection should become empty, test it explicitly.


13. Object Graph and Redaction Tests

When mapping from domain/entity to response, prove sensitive fields do not leak.

@Test
void accountResponse_doesNotExposeSensitiveFields() throws Exception {
    Account account = fixtureAccountWithSecrets();

    AccountResponse response = mapper.toResponse(account);
    String json = objectMapper.writeValueAsString(response);

    assertThat(json).doesNotContain("passwordHash");
    assertThat(json).doesNotContain("accessToken");
    assertThat(json).doesNotContain("internalRiskScore");
}

Also test graph depth:

@Test
void orderResponse_doesNotIncludeCustomerOrdersBackReference() throws Exception {
    Order order = fixtureOrderWithBidirectionalCustomer();

    OrderResponse response = mapper.toResponse(order);
    String json = objectMapper.writeValueAsString(response);

    assertThat(json).doesNotContain("orders");
}

14. Contract Fixture Tests

Fixture-driven test:

@Test
void providerPaymentFixture_mapsToCommand() throws Exception {
    ProviderPaymentDto dto = objectMapper.readValue(
        fixture("provider/payment-paid-v1.json"),
        ProviderPaymentDto.class
    );

    PaymentImportedCommand command = mapper.toCommand(dto);

    assertThat(command.paymentId().value()).isEqualTo("PAY-001");
    assertThat(command.money().amount()).isEqualByComparingTo("100.00");
    assertThat(command.status()).isEqualTo(PaymentStatus.COMPLETED);
}

Fixtures should include:

provider/payment-paid-v1.json
provider/payment-paid-v1-extra-field.json
provider/payment-paid-v1-unknown-status.json
provider/payment-paid-v1-invalid-amount.json
provider/payment-paid-v1-missing-currency.json

For XML:

case-report-valid.xml
case-report-wrong-namespace.xml
case-report-missing-required.xml
case-report-invalid-date.xml
case-report-xxe.xml

15. Property-Based Testing Ideas

Property-based testing means generate many inputs and assert invariant.

Example invariant: branch code preserves leading zeros.

@Property
void branchCodeRoundTripPreservesLeadingZeros(@ForAll("branchCodes") String raw) {
    BranchCode code = mapper.toBranchCode(raw);

    assertThat(mapper.fromBranchCode(code)).isEqualTo(raw);
}

Money invariant:

@Property
void moneyDtoRoundTripPreservesAmountAndCurrency(@ForAll MoneyDto dto) {
    assumeValidMoneyDto(dto);

    Money money = mapper.toMoney(dto);
    MoneyDto roundTrip = mapper.toDto(money);

    assertThat(roundTrip.amount()).isEqualTo(dto.amount());
    assertThat(roundTrip.currency()).isEqualTo(dto.currency());
}

Useful invariants:

MappingProperty
identifierround-trip preserves string
enum codeall known code maps to known enum
moneyno float/double precision loss
date-timeinstant stays same after round-trip
collectionsize/order preserved where required
redactionsecret never appears in serialized output
patchabsent field never changes target

Use property tests for conversion functions, not every mapper.


16. Mutation Surface Analysis

Mutation surface adalah bagian mapping yang jika salah akan merusak data atau contract.

High mutation surface fields:

  • identifiers
  • money
  • currency
  • status/state
  • permission flags
  • timestamps
  • customer/account references
  • nested ownership relation
  • external provider code
  • audit fields
  • nullable patch fields
  • polymorphic discriminator
  • secret/redacted fields

For each high-risk field, write explicit assertion.

Example checklist:

Payment mapper mutation surface:
- paymentId: preserves external id, not internal id
- amount: decimal string to BigDecimal, no double
- currency: ISO code, uppercase required
- status: explicit provider code map
- occurredAt: ISO instant, offset required
- customerId: business id, not DB id
- rawProviderStatus: preserved when unknown

17. Generated Code Review as Test Aid

For critical mapper:

  1. Open generated implementation.
  2. Check null handling.
  3. Check nested mapping path.
  4. Check enum switch.
  5. Check collection loop.
  6. Check ignored fields.
  7. Check constructor/builder use.
  8. Check unexpected implicit conversion.

Generated code is not usually committed, but it is reviewable in local/CI artifacts.

Add a PR checklist item:

For MapStruct mapper affecting payment/event/security/patch, generated code inspected? yes/no

18. Testing MapStruct with Spring/DI

For pure mapper tests, use generated mapper directly:

private final CustomerMapper mapper = Mappers.getMapper(CustomerMapper.class);

For Spring component model with dependencies/decorators, use Spring test or instantiate with dependencies manually.

@SpringBootTest
class PaymentMapperSpringTest {
    @Autowired PaymentMapper mapper;

    @Test
    void mapsWithInjectedDependencies() {
        // test
    }
}

Use Spring integration tests when:

  • mapper has decorator
  • mapper uses Spring component dependencies
  • mapper has injected helper classes
  • mapper config/injection matters

Use fast unit tests for pure mapping logic.


19. Contract Drift Tests

If target DTO adds new required field, strict MapStruct policy should fail compile. But if field is nullable or ignored, compile may pass. Add golden tests.

Example failure you want to catch:

public record PaymentApprovedEvent(
    String eventId,
    String paymentId,
    String status,
    String schemaVersion,
    String newRequiredField
) {}

If mapper sets newRequiredField to null, golden event test fails.

assertThatJson(actual).isEqualTo(expectedFixture);

Contract drift tests are especially important for event producers.


20. Mapper Tests and Validation Tests

Keep responsibilities separate.

Validation test:

@Test
void request_rejectsBlankCustomerId() {
    CreateCustomerRequest request = new CreateCustomerRequest("", "Ana");

    assertThat(validator.validate(request))
        .extracting(v -> v.getPropertyPath().toString())
        .contains("customerId");
}

Mapper test:

@Test
void validRequest_mapsToCommand() {
    CreateCustomerRequest request = new CreateCustomerRequest("CUS-001", "Ana");

    CreateCustomerCommand command = mapper.toCommand(request);

    assertThat(command.customerId().value()).isEqualTo("CUS-001");
}

Integration test:

invalid JSON -> deserialization/validation error -> mapper not called

Do not make mapper tests responsible for every validation rule.


21. Negative Testing

Positive tests prove expected data maps. Negative tests prove dangerous data does not pass silently.

Examples:

@Test
void mapperRejectsUnknownProviderStatus() {}

@Test
void mapperDoesNotExposePasswordHash() {}

@Test
void mapperDoesNotDefaultMissingAmountToZero() {}

@Test
void patchAbsentFieldDoesNotChangeTarget() {}

@Test
void invalidDateWithoutOffsetRejected() {}

@Test
void branchCodeLeadingZeroPreserved() {}

Negative tests are often more valuable than happy path tests for mapper correctness.


22. Performance Tests for Hot Mappers

Most mappers do not need performance tests. Some do:

  • import millions of rows
  • event ingestion hot path
  • export large files
  • object graph projections
  • map-heavy API gateway

Metrics:

  • records/sec
  • allocation rate
  • p95/p99 latency
  • GC pressure
  • payload size
  • max heap

Test shape:

@Test
void mapsLargeBatchWithinBudget() {
    List<ProviderPaymentDto> dtos = fixturePayments(100_000);

    long start = System.nanoTime();
    List<PaymentImportedCommand> commands = dtos.stream()
        .map(mapper::toCommand)
        .toList();
    long elapsed = System.nanoTime() - start;

    assertThat(commands).hasSize(100_000);
    // Use benchmark framework for real performance gates.
}

For serious performance, use JMH or integration benchmark, not naive unit timing only.


23. Test Naming Convention

Good test names encode contract.

Bad:

@Test void testMapper() {}

Good:

@Test void mapsProviderPaidStatusToCompleted() {}
@Test void preservesLeadingZerosInAccountNumber() {}
@Test void patchNullPhoneClearsPhone() {}
@Test void mergeNullPhoneKeepsExistingPhone() {}
@Test void responseDoesNotExposeInternalRiskScore() {}
@Test void eventGoldenJsonRemainsBackwardCompatible() {}

Test names become living documentation.


24. CI Gates

Mapper quality gates:

- Compile with MapStruct strict policies.
- Run mapper unit tests.
- Run fixture/golden tests.
- Run validation tests.
- Run contract compatibility tests for public events/API.
- Run security/redaction tests.
- Run mutation testing for critical mapper if useful.

CI should fail on:

  • unmapped target property
  • golden fixture diff
  • secret leak
  • unknown enum not handled
  • schema invalid generated XML
  • patch semantics regression

25. Mini Case Study: Payment Event Mapper Test Plan

Mapper:

PaymentApprovedEvent toApprovedEvent(Payment payment);

Risk surface:

  • event id
  • payment id
  • customer id
  • amount/currency
  • status
  • occurredAt
  • schemaVersion
  • no card token
  • no internal risk score

Tests:

1. maps required fields
2. amount serializes as decimal string
3. currency is ISO code
4. occurredAt is ISO instant
5. schemaVersion constant is v1
6. card token absent from JSON
7. internal risk score absent from JSON
8. golden JSON equals fixture
9. null optional note omitted/included per contract
10. event remains deserializable by v1 consumer fixture

Example:

@Test
void approvedEvent_doesNotExposeCardToken() throws Exception {
    Payment payment = fixturePaymentWithCardToken("tok_secret");

    PaymentApprovedEvent event = mapper.toApprovedEvent(payment);
    String json = objectMapper.writeValueAsString(event);

    assertThat(json).doesNotContain("tok_secret");
    assertThat(json).doesNotContain("cardToken");
}

26. Practice Drill

For this mapper:

ProviderTransferDto -> TransferImportedCommand

Fields:

transfer_id: String
source_account: String
destination_account: String
amount: String
currency: String
status: String
created_at: String
metadata: Map<String, String>

Design a test suite:

  1. Valid fixture maps to command.
  2. Leading zeros in account numbers preserved.
  3. Amount scale > 2 rejected.
  4. Lowercase currency rejected or normalized by explicit policy.
  5. Unknown status preserved or rejected by policy.
  6. Timestamp without offset rejected.
  7. Metadata secret key rejected or redacted.
  8. Golden imported command/event fixture stable.
  9. Null amount does not become zero.
  10. Collection/order behavior if there are transfer lines.

27. Summary

Mapper testing is about preserving meaning.

Mental model:

MapStruct verifies structure at compile time; tests verify semantic intent at runtime.

Rules:

  1. Classify mapper risk.
  2. Use strict compile policies.
  3. Write semantic assertions for high-risk fields.
  4. Use golden fixtures for public API/event/export contracts.
  5. Test null, absence, empty, and default behavior.
  6. Test enum coverage and unknown enum strategy.
  7. Test money, time, identifier, and status conversions deeply.
  8. Test redaction and graph depth.
  9. Inspect generated code for critical mappers.
  10. Keep validation tests separate from mapper tests, with integration tests for full pipeline.
  11. Treat golden diff as contract review.
  12. Use property-based checks for conversion invariants.

Part berikutnya starts Jakarta Validation: constraint, validator, violation, path, groups, payload, validator factory, and how validation fits into Java boundary engineering.


References

Lesson Recap

You just completed lesson 27 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.