Skip to content

Commit

Permalink
Introduce MethodInvoker API for TestExecutionListeners
Browse files Browse the repository at this point in the history
In order to be able to support parameter injection in
@​BeforeTransaction and @​AfterTransaction methods (see spring-projectsgh-30736), this
commit introduces a MethodInvoker API for TestExecutionListeners as a
generic mechanism for delegating to the underlying testing framework to
invoke methods.

The default implementation simply invokes the method without arguments,
which allows TestExecutionListeners using this mechanism to operate
correctly when the underlying testing framework is JUnit 4, TestNG, etc.

A JUnit Jupiter specific implementation is registered in the
SpringExtension which delegates to the
ExtensionContext.getExecutableInvoker() mechanism introduced in JUnit
Jupiter 5.9. This allows a TestExecutionListener to transparently
benefit from registered ParameterResolvers in JUnit Jupiter (including
the SpringExtension) when invoking user methods, effectively providing
support for parameter injection for arbitrary methods.

Closes spring-projectsgh-31199
  • Loading branch information
sbrannen committed Sep 10, 2023
1 parent 0ebdd8c commit 41904d4
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2002-2023 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.context;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
* Default implementation of the {@link MethodInvoker} API.
*
* <p>This implementation never provides arguments to a {@link Method}.
*
* @author Sam Brannen
* @since 6.1
*/
final class DefaultMethodInvoker implements MethodInvoker {

private static final Log logger = LogFactory.getLog(DefaultMethodInvoker.class);


@Override
@Nullable
public Object invoke(Method method, @Nullable Object target) throws Exception {
Assert.notNull(method, "Method must not be null");

try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target);
}
catch (InvocationTargetException ex) {
if (logger.isErrorEnabled()) {
logger.error("Exception encountered while invoking method [%s] on target [%s]"
.formatted(method, target), ex.getTargetException());
}
ReflectionUtils.rethrowException(ex.getTargetException());
// appease the compiler
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2002-2023 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.context;

import java.lang.reflect.Method;

import org.springframework.lang.Nullable;

/**
* {@code MethodInvoker} defines a generic API for invoking a {@link Method}
* within the <em>Spring TestContext Framework</em>.
*
* <p>Specifically, a {@code MethodInvoker} is made available to a
* {@link TestExecutionListener} via {@link TestContext#getMethodInvoker()}, and
* a {@code TestExecutionListener} can use the invoker to transparently benefit
* from any special method invocation features of the underlying testing framework.
*
* <p>For example, when the underlying testing framework is JUnit Jupiter, a
* {@code TestExecutionListener} can use a {@code MethodInvoker} to invoke
* arbitrary methods with JUnit Jupiter's
* {@linkplain org.junit.jupiter.api.extension.ExecutableInvoker parameter resolution
* mechanism}. For other testing frameworks, the {@link #DEFAULT_INVOKER} will be
* used.
*
* @author Sam Brannen
* @since 6.1
* @see org.junit.jupiter.api.extension.ExecutableInvoker
* @see org.springframework.util.MethodInvoker
*/
public interface MethodInvoker {

/**
* Shared instance of the default {@link MethodInvoker}.
* <p>This invoker never provides arguments to a {@link Method}.
*/
static final MethodInvoker DEFAULT_INVOKER = new DefaultMethodInvoker();


/**
* Invoke the supplied {@link Method} on the supplied {@code target}.
* <p>When the {@link #DEFAULT_INVOKER} is used &mdash; for example, when
* the underlying testing framework is JUnit 4 or TestNG &mdash; the method
* must not declare any formal parameters. When the underlying testing
* framework is JUnit Jupiter, parameters will be dynamically resolved via
* registered {@link org.junit.jupiter.api.extension.ParameterResolver
* ParameterResolvers} (such as the
* {@link org.springframework.test.context.junit.jupiter.SpringExtension
* SpringExtension}).
* @param method the method to invoke
* @param target the object on which to invoke the method, may be {@code null}
* if the method is {@code static}
* @return the value returned from the method invocation, potentially {@code null}
* @throws Exception if any error occurs
*/
@Nullable
Object invoke(Method method, @Nullable Object target) throws Exception;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -38,6 +38,9 @@
* that does not provide a copy constructor will likely fail in an environment
* that executes tests concurrently.
*
* <p>As of Spring Framework 6.1, concrete implementations are highly encouraged to
* override {@link #setMethodInvoker(MethodInvoker)} and {@link #getMethodInvoker()}.
*
* @author Sam Brannen
* @since 2.5
* @see TestContextManager
Expand Down Expand Up @@ -150,4 +153,28 @@ default void publishEvent(Function<TestContext, ? extends ApplicationEvent> even
*/
void updateState(@Nullable Object testInstance, @Nullable Method testMethod, @Nullable Throwable testException);

/**
* Set the {@link MethodInvoker} to use.
* <p>By default, this method does nothing.
* <p>Concrete implementations should track the supplied {@code MethodInvoker}
* and return it from {@link #getMethodInvoker()}. Note that the standard
* {@code TestContext} implementation in Spring overrides this method appropriately.
* @since 6.1
*/
default void setMethodInvoker(MethodInvoker methodInvoker) {
/* no-op */
}

/**
* Get the {@link MethodInvoker} to use.
* <p>By default, this method returns {@link MethodInvoker#DEFAULT_INVOKER}.
* <p>Concrete implementations should return the {@code MethodInvoker} supplied
* to {@link #setMethodInvoker(MethodInvoker)}. Note that the standard
* {@code TestContext} implementation in Spring overrides this method appropriately.
* @since 6.1
*/
default MethodInvoker getMethodInvoker() {
return MethodInvoker.DEFAULT_INVOKER;
}

}
Loading

0 comments on commit 41904d4

Please sign in to comment.