package com.ankurm.mapstruct; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.ankurm.mapstruct.domain.Customer; import com.ankurm.mapstruct.dto.AddressDto; import com.ankurm.mapstruct.dto.CustomerDto; import com.ankurm.mapstruct.mapper.AddressMapper; import com.ankurm.mapstruct.mapper.AddressMapperImpl; import com.ankurm.mapstruct.mapper.CustomerMapper; import com.ankurm.mapstruct.mapper.CustomerMapperImpl; import com.ankurm.mapstruct.mapper.OrderMapperImpl; import java.time.Clock; import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.function.Consumer; import java.util.function.Function; import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; /** Four ways to test a generated mapper, and one way to make a new DTO component fail the build until it is tested. */ class TestingStrategiesTests { // ---- 1. plain unit tests --------------------------------------------------------------------------- @Test void plainUnitTests() { try (var t = new Transcript("11-testing-mappers-plain.txt", "Testing a generated mapper without Spring: Mappers.getMapper versus constructing the Impl")) { AddressMapper viaFactory = Mappers.getMapper(AddressMapper.class); t.section("1. Mappers.getMapper(...)"); t.line("AddressMapper (no dependencies) : %s", viaFactory.getClass().getSimpleName()); Throwable thrown = null; try { Mappers.getMapper(CustomerMapper.class); } catch (RuntimeException e) { thrown = e; } t.line("CustomerMapper (uses = {...}) : %s", thrown == null ? "worked" : thrown.getClass().getSimpleName()); Throwable cause = thrown; while (cause != null && cause.getCause() != null) { cause = cause.getCause(); } t.line(" root cause : %s", cause == null ? "-" : cause.getClass().getName() + ": " + cause.getMessage()); CustomerMapper byHand = new CustomerMapperImpl(new AddressMapperImpl(), new OrderMapperImpl()); t.section("2. new CustomerMapperImpl(new AddressMapperImpl(), new OrderMapperImpl())"); t.line("no Spring, no reflection : %s", byHand.toDto(Fixtures.customer(), Fixtures.clock()).fullName()); assertThat(viaFactory).isInstanceOf(AddressMapperImpl.class); assertThat(thrown).isNotNull(); assertThat(cause).isInstanceOf(NoSuchMethodException.class); assertThat(byHand.toDto(Fixtures.customer(), Fixtures.clock()).fullName()).isEqualTo("Asha Mehta"); } } // ---- a property-level strategy ------------------------------------------------------------------ /** One mutation per DTO component: change the entity, and exactly that component must change. */ private static Map mutations() { Map m = new LinkedHashMap<>(); m.put("id", new Mutation(c -> c.setId(42L), CustomerDto::id)); m.put("fullName", new Mutation(c -> c.setLastName("Shah"), CustomerDto::fullName)); m.put("email", new Mutation(c -> c.setEmail("other@example.com"), CustomerDto::email)); m.put("birthDate", new Mutation(c -> c.setBirthDate(LocalDate.of(2000, 1, 1)), CustomerDto::birthDate)); m.put("age", new Mutation(c -> c.setBirthDate(LocalDate.of(2000, 1, 1)), CustomerDto::age)); m.put("nickname", new Mutation(c -> c.setNickname("ash"), CustomerDto::nickname)); m.put("address", new Mutation(c -> c.getAddress().setCity("Nagpur"), CustomerDto::address)); m.put("roles", new Mutation(c -> c.getRoles().add("ADMIN"), CustomerDto::roles)); m.put("attributes", new Mutation(c -> c.getAttributes().put("k", "v"), CustomerDto::attributes)); m.put("orders", new Mutation(c -> c.getOrders().get(0).getLines().get(0).setQuantity(9), CustomerDto::orders)); return m; } private record Mutation(Consumer change, Function read) { } @Test void everyDtoComponentIsCoveredByAMutationTest() { try (var t = new Transcript("14-property-level-test-strategy.txt", "Property-level mapper test: change one entity property, expect exactly the right DTO component to change")) { var mapper = Fixtures.customerMapper(); Clock clock = Fixtures.clock(); CustomerDto base = mapper.toDto(Fixtures.customer(), clock); List failures = new ArrayList<>(); mutations().forEach((component, mutation) -> { Customer changed = Fixtures.customer(); mutation.change().accept(changed); CustomerDto after = mapper.toDto(changed, clock); boolean moved = !mutation.read().apply(after).equals(mutation.read().apply(base)); t.line("%-11s changes when its source changes : %s", component, moved); if (!moved) { failures.add(component); } }); var components = new TreeSet(); for (var rc : CustomerDto.class.getRecordComponents()) { components.add(rc.getName()); } components.remove("origin"); // a constant: there is no source property to change t.blank(); t.line("record components : %s", components); t.line("covered by a test : %s", new TreeSet<>(mutations().keySet())); assertThat(failures).isEmpty(); assertThat(new TreeSet<>(mutations().keySet())).isEqualTo(components); assertThat(base.origin()).isEqualTo("api"); } } @Test void mapperIsUsableThroughItsInterfaceOnly() { // The same assertion style a team would copy: build the expected record by hand. CustomerDto dto = Fixtures.customerMapper().toDto(Fixtures.customer(), Fixtures.clock()); assertThat(dto.address()).isEqualTo(new AddressDto("12 Park Road", "Pune", "411001", "IN")); assertThatThrownBy(() -> Mappers.getMapper(CustomerMapper.class)).isInstanceOf(RuntimeException.class); } }