diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc index 0ef8443eaea3..adf21b0f34f5 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/executing-sql.adoc @@ -417,13 +417,29 @@ If the algorithms used by `SqlScriptsTestExecutionListener` to detect a `DataSou you can specify explicit names by setting the `dataSource` and `transactionManager` attributes of `@SqlConfig`. Furthermore, you can control the transaction propagation behavior by setting the `transactionMode` attribute of `@SqlConfig` (for example, whether -scripts should be run in an isolated transaction). Although a thorough discussion of all -supported options for transaction management with `@Sql` is beyond the scope of this -reference manual, the javadoc for +scripts should be run in an isolated transaction). See the Javadoc for {spring-framework-api}/test/context/jdbc/SqlConfig.html[`@SqlConfig`] and {spring-framework-api}/test/context/jdbc/SqlScriptsTestExecutionListener.html[`SqlScriptsTestExecutionListener`] -provide detailed information, and the following example shows a typical testing scenario -that uses JUnit Jupiter and transactional tests with `@Sql`: +for the complete reference. + +[[testcontext-executing-sql-declaratively-tx-inferred]] +===== `INFERRED` transaction mode + +The `INFERRED` mode (which is the effective default) determines the transaction behavior +automatically based on which beans are available in the test's `ApplicationContext`: + +* If neither a transaction manager nor a data source is available, an exception is thrown. +* If a transaction manager is not available but a data source is available, SQL scripts + are executed directly against the data source without a transaction. +* If a transaction manager is available, scripts are executed within an existing + Spring-managed transaction if one is active (for example, when the test method is + annotated with `@Transactional`); otherwise, a new transaction is started and + immediately committed. + +The following example shows a typical testing scenario that uses JUnit Jupiter and +transactional tests with `@Sql`. With `INFERRED` (the default), the `/test-data.sql` +script participates in the same transaction as the test method and its changes are +automatically rolled back afterward: [tabs] ====== @@ -497,6 +513,76 @@ run, since any changes made to the database (either within the test method or wi `TransactionalTestExecutionListener` (see xref:testing/testcontext-framework/tx.adoc[transaction management] for details). +[[testcontext-executing-sql-declaratively-tx-isolated]] +===== `ISOLATED` transaction mode + +Use `ISOLATED` when SQL scripts must be committed to the database _before_ the test +method's own transaction is able to see the changes, or when the data set up by the +script must persist after the test method's transaction has rolled back. + +A common scenario is testing code that spawns a new transaction internally. Because the +test method's transaction and the code-under-test's transaction are separate, the +code-under-test can only see data that has been committed. With the `INFERRED` mode the +SQL setup script runs within the test's transaction, so its data is not yet committed +and therefore invisible to the code-under-test. Using `ISOLATED` causes the script to +run in a new, separate transaction that is committed immediately, making the data visible +across all subsequent connections. + +`ISOLATED` requires both a `PlatformTransactionManager` and a `DataSource` to be +present in the test's `ApplicationContext`. The following example demonstrates the +pattern: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestDatabaseConfig.class) + class IsolatedSqlScriptsTests { + + @Test + @Sql( + scripts = "/test-data.sql", + config = @SqlConfig(transactionMode = ISOLATED) + ) + @Sql( + scripts = "/cleanup-test-data.sql", + config = @SqlConfig(transactionMode = ISOLATED), + executionPhase = AFTER_TEST_METHOD + ) + void testCodeThatOpensItsOwnTransaction() { + // The test data was committed before this method runs, so the + // code-under-test can see it even in a separate transaction. + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestDatabaseConfig::class) + class IsolatedSqlScriptsTests { + + @Test + @Sql("/test-data.sql", config = SqlConfig(transactionMode = ISOLATED)) + @Sql( + "/cleanup-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED), + executionPhase = AFTER_TEST_METHOD + ) + fun testCodeThatOpensItsOwnTransaction() { + // The test data was committed before this method runs, so the + // code-under-test can see it even in a separate transaction. + } + } +---- +====== + +NOTE: `ISOLATED`, `AFTER_TEST_METHOD`, and `INFERRED` are statically imported from +`SqlConfig.TransactionMode` and `Sql.ExecutionPhase`, respectively. + [[testcontext-executing-sql-declaratively-script-merging]] === Merging and Overriding Configuration with `@SqlMergeMode` 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 a98f0f72f9cf..cd30ea363a69 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 @@ -493,6 +493,80 @@ Kotlin:: ====== +The following example demonstrates `@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)`. +In this scenario, the `SpecialCaseTests` inner class needs a different Spring configuration +than the enclosing class and therefore declares its own `@SpringJUnitConfig` annotation, +overriding the one inherited from `GreetingServiceTests`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig.class) + class GreetingServiceTests { + + @Nested + @ActiveProfiles("lang_en") + class EnglishGreetings { + + @Test + void hello(@Autowired GreetingService service) { + assertThat(service.greetWorld()).isEqualTo("Hello World"); + } + } + + // Opt out of inheriting configuration from the enclosing class and + // supply a dedicated configuration for this nested test class. + @Nested + @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) + @SpringJUnitConfig(SpecialConfig.class) + class SpecialCaseTests { + + @Test + void specialCase(@Autowired SpecialService service) { + // test using SpecialConfig, not TestConfig + } + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @SpringJUnitConfig(TestConfig::class) + class GreetingServiceTests { + + @Nested + @ActiveProfiles("lang_en") + inner class EnglishGreetings { + + @Test + fun hello(@Autowired service: GreetingService) { + assertThat(service.greetWorld()).isEqualTo("Hello World") + } + } + + // Opt out of inheriting configuration from the enclosing class and + // supply a dedicated configuration for this nested test class. + @Nested + @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) + @SpringJUnitConfig(SpecialConfig::class) + inner class SpecialCaseTests { + + @Test + fun specialCase(@Autowired service: SpecialService) { + // test using SpecialConfig, not TestConfig + } + } + } +---- +====== + +NOTE: `EnclosingConfiguration` is statically imported from `NestedTestConfiguration.EnclosingConfiguration`. + [[testcontext-junit4-support]] == JUnit 4 Support