diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java index f922c8a34a4..42eeb706cbd 100644 --- a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -403,14 +403,65 @@ public abstract class ReflectionTestUtils { /** * Invoke the method with the given {@code name} on the supplied target * object with the supplied arguments. - *

This method traverses the class hierarchy in search of the desired - * method. In addition, an attempt will be made to make non-{@code public} - * methods accessible, thus allowing one to invoke {@code protected}, - * {@code private}, and package-private methods. + *

This method delegates to {@link #invokeMethod(Object, Class, String, Object...)}, + * supplying {@code null} for the {@code targetClass} argument. * @param target the target object on which to invoke the specified method * @param name the name of the method to invoke * @param args the arguments to provide to the method * @return the invocation result, if any + * @see #invokeMethod(Class, String, Object...) + * @see #invokeMethod(Object, Class, String, Object...) + * @see MethodInvoker + * @see ReflectionUtils#makeAccessible(Method) + * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + * @see ReflectionUtils#handleReflectionException(Exception) + */ + @Nullable + public static T invokeMethod(Object target, String name, Object... args) { + Assert.notNull(target, "Target object must not be null"); + return invokeMethod(target, null, name, args); + } + + /** + * Invoke the static method with the given {@code name} on the supplied target + * class with the supplied arguments. + *

This method delegates to {@link #invokeMethod(Object, Class, String, Object...)}, + * supplying {@code null} for the {@code targetObject} argument. + * @param targetClass the target class on which to invoke the specified method + * @param name the name of the method to invoke + * @param args the arguments to provide to the method + * @return the invocation result, if any + * @since 5.2 + * @see #invokeMethod(Object, String, Object...) + * @see #invokeMethod(Object, Class, String, Object...) + * @see MethodInvoker + * @see ReflectionUtils#makeAccessible(Method) + * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) + * @see ReflectionUtils#handleReflectionException(Exception) + */ + @Nullable + public static T invokeMethod(Class targetClass, String name, Object... args) { + Assert.notNull(targetClass, "Target class must not be null"); + return invokeMethod(null, targetClass, name, args); + } + + /** + * Invoke the method with the given {@code name} on the provided + * {@code targetObject}/{@code targetClass} with the supplied arguments. + *

This method traverses the class hierarchy in search of the desired + * method. In addition, an attempt will be made to make non-{@code public} + * methods accessible, thus allowing one to invoke {@code protected}, + * {@code private}, and package-private methods. + * @param targetObject the target object on which to invoke the method; may + * be {@code null} if the method is static + * @param targetClass the target class on which to invoke the method; may + * be {@code null} if the method is an instance method + * @param name the name of the method to invoke + * @param args the arguments to provide to the method + * @return the invocation result, if any + * @since 5.2 + * @see #invokeMethod(Object, String, Object...) + * @see #invokeMethod(Class, String, Object...) * @see MethodInvoker * @see ReflectionUtils#makeAccessible(Method) * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) @@ -418,20 +469,26 @@ public abstract class ReflectionTestUtils { */ @SuppressWarnings("unchecked") @Nullable - public static T invokeMethod(Object target, String name, Object... args) { - Assert.notNull(target, "Target object must not be null"); + public static T invokeMethod(@Nullable Object targetObject, @Nullable Class targetClass, String name, + Object... args) { + + Assert.isTrue(targetObject != null || targetClass != null, + "Either 'targetObject' or 'targetClass' for the method must be specified"); Assert.hasText(name, "Method name must not be empty"); try { MethodInvoker methodInvoker = new MethodInvoker(); - methodInvoker.setTargetObject(target); + methodInvoker.setTargetObject(targetObject); + if (targetClass != null) { + methodInvoker.setTargetClass(targetClass); + } methodInvoker.setTargetMethod(name); methodInvoker.setArguments(args); methodInvoker.prepare(); if (logger.isDebugEnabled()) { - logger.debug(String.format("Invoking method '%s' on %s with arguments %s", name, safeToString(target), - ObjectUtils.nullSafeToString(args))); + logger.debug(String.format("Invoking method '%s' on %s or %s with arguments %s", name, + safeToString(targetObject), safeToString(targetClass), ObjectUtils.nullSafeToString(args))); } return (T) methodInvoker.invoke(); @@ -452,4 +509,8 @@ public abstract class ReflectionTestUtils { } } + private static String safeToString(@Nullable Class clazz) { + return String.format("target class [%s]", (clazz != null ? clazz.getName() : null)); + } + } diff --git a/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java index 8261183dbf4..cdc504961db 100644 --- a/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/ReflectionTestUtilsTests.java @@ -27,6 +27,7 @@ import org.springframework.test.util.subpackage.LegacyEntity; import org.springframework.test.util.subpackage.Person; import org.springframework.test.util.subpackage.PersonEntity; import org.springframework.test.util.subpackage.StaticFields; +import org.springframework.test.util.subpackage.StaticMethods; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -57,6 +58,7 @@ class ReflectionTestUtilsTests { @BeforeEach void resetStaticFields() { StaticFields.reset(); + StaticMethods.reset(); } @Test @@ -327,7 +329,7 @@ class ReflectionTestUtilsTests { } @Test - @Disabled("[SPR-8644] findMethod() does not currently support var-args") + @Disabled("[SPR-8644] MethodInvoker.findMatchingMethod() does not currently support var-args") void invokeMethodWithPrimitiveVarArgs() { // IntelliJ IDEA 11 won't accept int assignment here Integer sum = invokeMethod(component, "add", 1, 2, 3, 4); @@ -422,4 +424,66 @@ class ReflectionTestUtilsTests { assertThat(entity.toString().contains(testCollaborator)).isTrue(); } + @Test + void invokeStaticMethodWithNullTargetClass() { + assertThatIllegalArgumentException() + .isThrownBy(() -> invokeMethod((Class) null, null)) + .withMessage("Target class must not be null"); + } + + @Test + void invokeStaticMethodWithNullMethodName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> invokeMethod(getClass(), null)) + .withMessage("Method name must not be empty"); + } + + @Test + void invokeStaticMethodWithEmptyMethodName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> invokeMethod(getClass(), " ")) + .withMessage("Method name must not be empty"); + } + + @Test + void invokePublicStaticVoidMethodWithArguments() { + assertThat(StaticMethods.getPublicMethodValue()).isEqualTo("public"); + + String testCollaborator = "test collaborator"; + invokeMethod(StaticMethods.class, "publicMethod", testCollaborator); + assertThat(StaticMethods.getPublicMethodValue()).isEqualTo(testCollaborator); + } + + @Test + void invokePublicStaticMethodWithoutArguments() { + assertThat(StaticMethods.getPublicMethodValue()).isEqualTo("public"); + + String result = invokeMethod(StaticMethods.class, "publicMethod"); + assertThat(result).isEqualTo(StaticMethods.getPublicMethodValue()); + } + + @Test + void invokePrivateStaticVoidMethodWithArguments() { + assertThat(StaticMethods.getPrivateMethodValue()).isEqualTo("private"); + + String testCollaborator = "test collaborator"; + invokeMethod(StaticMethods.class, "privateMethod", testCollaborator); + assertThat(StaticMethods.getPrivateMethodValue()).isEqualTo(testCollaborator); + } + + @Test + void invokePrivateStaticMethodWithoutArguments() { + assertThat(StaticMethods.getPrivateMethodValue()).isEqualTo("private"); + + String result = invokeMethod(StaticMethods.class, "privateMethod"); + assertThat(result).isEqualTo(StaticMethods.getPrivateMethodValue()); + } + + @Test + void invokeStaticMethodWithNullTargetObjectAndNullTargetClass() { + assertThatIllegalArgumentException() + .isThrownBy(() -> invokeMethod((Object) null, (Class) null, "id")) + .withMessage("Either 'targetObject' or 'targetClass' for the method must be specified"); + } + } diff --git a/spring-test/src/test/java/org/springframework/test/util/subpackage/StaticMethods.java b/spring-test/src/test/java/org/springframework/test/util/subpackage/StaticMethods.java new file mode 100644 index 00000000000..c1006e8e145 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/util/subpackage/StaticMethods.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.util.subpackage; + +/** + * Simple class with static methods; intended for use in unit tests. + * + * @author Sam Brannen + * @since 5.2 + */ +public class StaticMethods { + + public static String publicMethodValue = "public"; + + private static String privateMethodValue = "private"; + + + public static void publicMethod(String value) { + publicMethodValue = value; + } + + public static String publicMethod() { + return publicMethodValue; + } + + @SuppressWarnings("unused") + private static void privateMethod(String value) { + privateMethodValue = value; + } + + @SuppressWarnings("unused") + private static String privateMethod() { + return privateMethodValue; + } + + public static void reset() { + publicMethodValue = "public"; + privateMethodValue = "private"; + } + + public static String getPublicMethodValue() { + return publicMethodValue; + } + + public static String getPrivateMethodValue() { + return privateMethodValue; + } + +}