125 lines
6.3 KiB
Java
125 lines
6.3 KiB
Java
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<String, Mutation> mutations() {
|
|
Map<String, Mutation> 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("[email protected]"), 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<Customer> change, Function<CustomerDto, Object> 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<String> 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<String>();
|
|
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);
|
|
}
|
|
}
|