From e7f019bd3f0c3c051c5c7cb2874c41307b92da62 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:12:59 +0100 Subject: [PATCH] Introduce tests for PropertyDescriptorUtils with bounded generics As a follow up to commit 4b07edbaeb, this commit introduces tests for PropertyDescriptorUtils.determineBasicProperties() using types with bounded generics. Note, however, that the following test effectively fails, since PropertyDescriptorUtils.determineBasicProperties() does not match the behavior of java.beans.Introspector. Consequently, this test method currently changes the expected write method type conditionally. resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter() See gh-36019 --- ...escriptorUtilsPropertyResolutionTests.java | 194 ++++++++++++++---- 1 file changed, 157 insertions(+), 37 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyDescriptorUtilsPropertyResolutionTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyDescriptorUtilsPropertyResolutionTests.java index 66207423f1b..5b0e1775e20 100644 --- a/spring-beans/src/test/java/org/springframework/beans/PropertyDescriptorUtilsPropertyResolutionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/PropertyDescriptorUtilsPropertyResolutionTests.java @@ -18,12 +18,14 @@ package org.springframework.beans; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.Parameter; import org.junit.jupiter.params.ParameterizedClass; @@ -58,58 +60,118 @@ class PropertyDescriptorUtilsPropertyResolutionTests { PropertiesResolver resolver; - @Test - void determineBasicPropertiesWithUnresolvedGenericsInInterface() { - var pdMap = resolver.resolve(GenericService.class); + @Nested + class UnboundedGenericsTests { - assertThat(pdMap).containsOnlyKeys("id"); - assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class); - } + @Test + void determineBasicPropertiesWithUnresolvedGenericsInInterface() { + var pdMap = resolver.resolve(GenericService.class); - @Test - void determineBasicPropertiesWithUnresolvedGenericsInSubInterface() { - // FYI: java.beans.Introspector does not resolve properties for sub-interfaces. - assumeThat(resolver).isNotInstanceOf(StandardPropertiesResolver.class); + assertThat(pdMap).containsOnlyKeys("id"); + assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class); + } - var pdMap = resolver.resolve(SubGenericService.class); + @Test + void determineBasicPropertiesWithUnresolvedGenericsInSubInterface() { + // FYI: java.beans.Introspector does not resolve properties for sub-interfaces. + assumeThat(resolver).isNotInstanceOf(StandardPropertiesResolver.class); - assertThat(pdMap).containsOnlyKeys("id"); - assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class); - } + var pdMap = resolver.resolve(SubGenericService.class); - @Test - void resolvePropertiesWithUnresolvedGenericsInClass() { - var pdMap = resolver.resolve(BaseService.class); + assertThat(pdMap).containsOnlyKeys("id"); + assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class); + } - assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class); - } + @Test + void resolvePropertiesWithUnresolvedGenericsInClass() { + var pdMap = resolver.resolve(BaseService.class); - @Test // gh-36019 - void resolvePropertiesInSubclassWithOverriddenGetterAndSetter() { - var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndSetter.class); + assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class); + } - assertReadAndWriteMethodsForClassAndId(pdMap, String.class, String.class); - } + @Test // gh-36019 + void resolvePropertiesInSubclassWithOverriddenGetterAndSetter() { + var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndSetter.class); - @Test // gh-36019 - void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter() { - var pdMap = resolver.resolve(ServiceWithOverloadedSetter.class); + assertReadAndWriteMethodsForClassAndId(pdMap, String.class, String.class); + } - assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class); - } + @Test // gh-36019 + void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter() { + var pdMap = resolver.resolve(ServiceWithOverloadedSetter.class); - @Test // gh-36019 - void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetter() { - var pdMap = resolver.resolve(ServiceWithOverriddenGetter.class); + assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class); + } + + @Test // gh-36019 + void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetter() { + var pdMap = resolver.resolve(ServiceWithOverriddenGetter.class); + + assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class); + } + + @Test // gh-36019 + void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetterAndOverloadedSetter() { + var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndOverloadedSetter.class); - assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class); + assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class); + } } - @Test // gh-36019 - void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetterAndOverloadedSetter() { - var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndOverloadedSetter.class); + @Nested + class BoundedGenericsTests { + + @Test + void determineBasicPropertiesWithUnresolvedGenericsInInterface() { + var pdMap = resolver.resolve(Entity.class); + + assertThat(pdMap).containsOnlyKeys("id"); + assertReadAndWriteMethodsForId(pdMap.get("id"), Serializable.class, Serializable.class); + } - assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class); + @Test + void resolvePropertiesWithUnresolvedGenericsInClass() { + var pdMap = resolver.resolve(BaseEntity.class); + + assertReadAndWriteMethodsForClassAndId(pdMap, Number.class, Number.class); + } + + @Test + void resolvePropertiesWithUnresolvedGenericsInSubclass() { + var pdMap = resolver.resolve(Person.class); + + assertReadAndWriteMethodsForClassAndId(pdMap, Number.class, Number.class); + } + + @Test // gh-36019 + void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverriddenGetter() { + var pdMap = resolver.resolve(PersonWithOverriddenGetter.class); + + assertReadAndWriteMethodsForClassAndId(pdMap, Long.class, Number.class); + } + + @Test // gh-36019 + void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverriddenSetter() { + var pdMap = resolver.resolve(PersonWithOverriddenSetter.class); + + assertReadAndWriteMethodsForClassAndId(pdMap, Number.class, Long.class); + } + + @Test // gh-36019 + void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter() { + var pdMap = resolver.resolve(PersonWithOverloadedSetter.class); + + // TODO Determine if we want to align PropertyDescriptorUtils with java.beans.Introspector. + Class writeType = Number.class; + if (resolver instanceof BasicPropertiesResolver) { + // PropertyDescriptorUtils currently incorrectly resolves setId(Integer) + // as the write method instead of setId(Number) (where Number is the + // unresolved generic for Long). + writeType = Integer.class; + } + + assertReadAndWriteMethodsForClassAndId(pdMap, Number.class, writeType); + } } @@ -262,4 +324,62 @@ class PropertyDescriptorUtilsPropertyResolutionTests { } } + interface Entity { + + T getId(); + + void setId(T id); + } + + abstract static class BaseEntity implements Entity { + + private T id; + + @Override + public T getId() { + return this.id; + } + + @Override + public void setId(T id) { + this.id = id; + } + } + + static class Person extends BaseEntity { + } + + static class PersonWithOverriddenGetter extends BaseEntity { + + /** + * Overrides super implementation to ensure that the JavaBeans read method + * is of type {@link Long}, while leaving the type for the write method + * ({@link #setId}) set to {@link Number}. + */ + @Override + public Long getId() { + return super.getId(); + } + } + + static class PersonWithOverriddenSetter extends BaseEntity { + + /** + * Overrides super implementation to ensure that the JavaBeans write method + * is of type {@link Long}, while leaving the type for the read method + * ({@link #getId()}) set to {@link Number}. + */ + @Override + public void setId(Long id) { + super.setId(id); + } + } + + static class PersonWithOverloadedSetter extends BaseEntity { + + // Intentionally chose Integer, since it's a subtype of Long and Number. + public void setId(Integer id) { + setId(id.longValue()); + } + } }