diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc index 7f0a8c19aa2..bbe1485f21c 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc @@ -177,7 +177,7 @@ following features above and beyond the feature set that Spring supports for JUn TestNG: * Dependency injection for test constructors, test methods, and test lifecycle callback - methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with `SpringExtension`] for further details. + methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details. * Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional test execution] based on SpEL expressions, environment variables, system properties, and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in @@ -310,17 +310,19 @@ See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details. [[testcontext-junit-jupiter-di]] -=== Dependency Injection with `SpringExtension` +=== Dependency Injection with the `SpringExtension` -`SpringExtension` implements the +The `SpringExtension` implements the link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`] extension API from JUnit Jupiter, which lets Spring provide dependency injection for test constructors, test methods, and test lifecycle callback methods. -Specifically, `SpringExtension` can inject dependencies from the test's -`ApplicationContext` into test constructors and methods that are annotated with -`@BeforeAll`, `@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, -`@ParameterizedTest`, and others. +Specifically, the `SpringExtension` can inject dependencies from the test's +`ApplicationContext` into into test constructors and methods that are annotated with +Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, +`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, +and others. + [[testcontext-junit-jupiter-di-constructor]] ==== Constructor Injection diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc index 1f1a9c676a0..d144ddc2f77 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc @@ -298,6 +298,44 @@ method in a test class or any `void` default method in a test interface with one annotations, and the `TransactionalTestExecutionListener` ensures that your before-transaction method or after-transaction method runs at the appropriate time. +[NOTE] +==== +Generally speaking, `@BeforeTransaction` and `@AfterTransaction` methods must not accept +any arguments. + +However, as of Spring Framework 6.1, for tests using the +xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] +with JUnit Jupiter, `@BeforeTransaction` and `@AfterTransaction` methods may optionally +accept arguments which will be resolved by any registered JUnit `ParameterResolver` +extension such as the `SpringExtension`. This means that JUnit-specific arguments like +`TestInfo` or beans from the test's `ApplicationContext` may be provided to +`@BeforeTransaction` and `@AfterTransaction` methods, as demonstrated in the following +example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +@BeforeTransaction +void verifyInitialDatabaseState(@Autowired DataSource dataSource) { + // Use the DataSource to verify the initial state before a transaction is started +} +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- +@BeforeTransaction +fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) { + // Use the DataSource to verify the initial state before a transaction is started +} +---- +====== +==== + [TIP] ==== Any before methods (such as methods annotated with JUnit Jupiter's `@BeforeEach`) and any diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java index 03f88348f6c..29b38c07180 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -23,20 +23,28 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - *
Test annotation which indicates that the annotated {@code void} method + * Test annotation which indicates that the annotated {@code void} method * should be executed after a transaction is ended for a test method * configured to run within a transaction via Spring's {@code @Transactional} * annotation. * + *
Generally speaking, {@code @AfterTransaction} methods must not accept any + * arguments. However, as of Spring Framework 6.1, for tests using the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * with JUnit Jupiter, {@code @AfterTransaction} methods may optionally accept + * arguments which will be resolved by any registered JUnit + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver} + * extension such as the {@code SpringExtension}. This means that JUnit-specific + * arguments like {@link org.junit.jupiter.api.TestInfo TestInfo} or beans from + * the test's {@code ApplicationContext} may be provided to {@code @AfterTransaction} + * methods analogous to {@code @AfterEach} methods. + * *
{@code @AfterTransaction} methods declared in superclasses or as interface * default methods will be executed after those of the current test class. * *
This annotation may be used as a meta-annotation to create custom * composed annotations. * - *
As of Spring Framework 4.3, {@code @AfterTransaction} may also be - * declared on Java 8 based interface default methods. - * * @author Sam Brannen * @since 2.5 * @see org.springframework.transaction.annotation.Transactional diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java index e04358b391a..59e68d3ba04 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -23,20 +23,28 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - *
Test annotation which indicates that the annotated {@code void} method + * Test annotation which indicates that the annotated {@code void} method * should be executed before a transaction is started for a test method * configured to run within a transaction via Spring's {@code @Transactional} * annotation. * + *
Generally speaking, {@code @BeforeTransaction} methods must not accept any + * arguments. However, as of Spring Framework 6.1, for tests using the + * {@link org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension} + * with JUnit Jupiter, {@code @BeforeTransaction} methods may optionally accept + * arguments which will be resolved by any registered JUnit + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolver} + * extension such as the {@code SpringExtension}. This means that JUnit-specific + * arguments like {@link org.junit.jupiter.api.TestInfo TestInfo} or beans from + * the test's {@code ApplicationContext} may be provided to {@code @BeforeTransaction} + * methods analogous to {@code @BeforeEach} methods. + * *
{@code @BeforeTransaction} methods declared in superclasses or as interface * default methods will be executed before those of the current test class. * *
This annotation may be used as a meta-annotation to create custom * composed annotations. * - *
As of Spring Framework 4.3, {@code @BeforeTransaction} may also be - * declared on Java 8 based interface default methods. - * * @author Sam Brannen * @since 2.5 * @see org.springframework.transaction.annotation.Transactional diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 0da5fc1eb8b..99a0a4ae559 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -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. @@ -287,8 +287,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis logger.debug("Executing @BeforeTransaction method [%s] for test class [%s]" .formatted(method, testClass.getName())); } - ReflectionUtils.makeAccessible(method); - method.invoke(testContext.getTestInstance()); + testContext.getMethodInvoker().invoke(method, testContext.getTestInstance()); } } catch (InvocationTargetException ex) { @@ -323,8 +322,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis logger.debug("Executing @AfterTransaction method [%s] for test class [%s]" .formatted(method, testClass.getName())); } - ReflectionUtils.makeAccessible(method); - method.invoke(testContext.getTestInstance()); + testContext.getMethodInvoker().invoke(method, testContext.getTestInstance()); } catch (InvocationTargetException ex) { Throwable targetException = ex.getTargetException(); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TransactionLifecycleMethodParameterInjectionTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TransactionLifecycleMethodParameterInjectionTests.java new file mode 100644 index 00000000000..1d2c29e684b --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/transaction/TransactionLifecycleMethodParameterInjectionTests.java @@ -0,0 +1,111 @@ +/* + * 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.junit.jupiter.transaction; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.transaction.TransactionAssert.assertThatTransaction; + +/** + * JUnit Jupiter based integration tests which verify support for parameter + * injection in {@link BeforeTransaction @BeforeTransaction} and + * {@link AfterTransaction @AfterTransaction} lifecycle methods. + * + * @author Sam Brannen + * @since 6.1 + */ +@SpringJUnitConfig +class TransactionLifecycleMethodParameterInjectionTests { + + static boolean beforeTransactionInvoked = false; + static boolean afterTransactionInvoked = false; + + + @BeforeAll + static void checkInitialFlagState() { + assertThat(beforeTransactionInvoked).isFalse(); + assertThat(afterTransactionInvoked).isFalse(); + } + + @BeforeTransaction + void beforeTransaction(TestInfo testInfo, ApplicationContext context, @Autowired DataSource dataSource) { + assertThatTransaction().isNotActive(); + assertThat(testInfo).isNotNull(); + assertThat(context).isNotNull(); + assertThat(dataSource).isNotNull(); + beforeTransactionInvoked = true; + } + + @Test + @Transactional + void transactionalTest(TestInfo testInfo, ApplicationContext context, @Autowired DataSource dataSource) { + assertThatTransaction().isActive(); + assertThat(testInfo).isNotNull(); + assertThat(context).isNotNull(); + assertThat(dataSource).isNotNull(); + assertThat(beforeTransactionInvoked).isTrue(); + assertThat(afterTransactionInvoked).isFalse(); + } + + @AfterTransaction + void afterTransaction(TestInfo testInfo, ApplicationContext context, @Autowired DataSource dataSource) { + assertThatTransaction().isNotActive(); + assertThat(testInfo).isNotNull(); + assertThat(context).isNotNull(); + assertThat(dataSource).isNotNull(); + afterTransactionInvoked = true; + } + + @AfterAll + static void checkFinalFlagState() { + assertThat(beforeTransactionInvoked).isTrue(); + assertThat(afterTransactionInvoked).isTrue(); + } + + + @Configuration + static class Config { + + @Bean + DataSourceTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean + DataSource dataSource() { + return new EmbeddedDatabaseBuilder().generateUniqueName(true).build(); + } + } + +}