From 5ce75f3d08be08536278e231b3ecacafd672dde8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 23 Aug 2019 16:27:27 +0200 Subject: [PATCH] Support static methods with ReflectionTestUtils.invokeMethod() Prior to this commit, the invokeMethod() utility method in ReflectionTestUtils only supported instance methods. This commit brings the invokeMethod() support on par with the getField() support by supporting the invocation of static methods via two new invokeMethod() variants. Closes gh-23504 --- .../test/util/ReflectionTestUtils.java | 81 ++++++++++++++++--- .../test/util/ReflectionTestUtilsTests.java | 66 ++++++++++++++- .../test/util/subpackage/StaticMethods.java | 63 +++++++++++++++ 3 files changed, 199 insertions(+), 11 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/util/subpackage/StaticMethods.java 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; + } + +}