Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions framework-docs/modules/ROOT/pages/core/beans/basics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ xref:core/beans/java.adoc[Java-based configuration] for their Spring application
{spring-framework-api}/context/annotation/Import.html[`@Import`],
and {spring-framework-api}/context/annotation/DependsOn.html[`@DependsOn`] annotations.

[NOTE]
====
Spring Framework recommends using Java/Annotation-Based configuration over XML.
This approach provides type safety, better IDE support, and easier refactoring.
XML configuration is still supported for legacy scenarios.
====
Comment on lines +53 to +58
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note about recommending Java/Annotation-Based configuration over XML appears unrelated to the PR's stated purpose of documenting profile-based conditional test execution. The PR description makes no mention of adding guidance about configuration approaches. This change should either be removed from this PR or the PR description should be updated to explain its inclusion.

Suggested change
[NOTE]
====
Spring Framework recommends using Java/Annotation-Based configuration over XML.
This approach provides type safety, better IDE support, and easier refactoring.
XML configuration is still supported for legacy scenarios.
====

Copilot uses AI. Check for mistakes.

Spring configuration consists of at least one and typically more than one bean definition
that the container must manage. Java configuration typically uses `@Bean`-annotated
methods within a `@Configuration` class, each corresponding to one bean definition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,101 @@ Kotlin::
----
======


[[testcontext-ctx-management-env-profiles-conditional-test-execution]]
== Conditional Test Execution Based on Active Profiles

In some scenarios, you may want to enable or disable entire test classes or individual
test methods based on active Spring profiles. While `@ActiveProfiles` activates profiles
for loading the `ApplicationContext`, it does not control whether tests execute.

When using JUnit Jupiter (JUnit 5), you can conditionally enable or disable tests based
on active profiles in two ways:

1. **Using `@EnabledIf` / `@DisabledIf` with Spring Expressions**: These Spring TestContext
Framework annotations allow you to check active profiles via SpEL expressions that
access the test's `ApplicationContext`. Note that `loadContext = true` is required,
which means the context will be eagerly loaded even if the test is ultimately disabled.

2. **Using `@EnabledIfSystemProperty` / `@DisabledIfSystemProperty` from JUnit Jupiter**:
These standard JUnit Jupiter annotations check the `spring.profiles.active` system
property without loading the Spring context. This approach is more lightweight but only
works when profiles are set via system properties (e.g., `-Dspring.profiles.active=oracle`).

The following example demonstrates both approaches:

[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.EnabledIf;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig
@ActiveProfiles("oracle")
class ProfileBasedTestExecutionTests {

// Approach 1: Using Spring's @EnabledIf with SpEL
// Requires loading the ApplicationContext (loadContext = true)
@Test
@EnabledIf(expression = "#{environment.matchesProfiles('oracle')}", loadContext = true)
void testOnlyForOracleProfile() {
// This test runs only when the 'oracle' profile is active
}

// Approach 2: Using JUnit Jupiter's @EnabledIfSystemProperty
// Lightweight approach that checks system property without loading context
// Run with: -Dspring.profiles.active=oracle
@Test
@EnabledIfSystemProperty(named = "spring.profiles.active", matches = "oracle")
void testOnlyWhenOracleSystemPropertySet() {
// This test runs only when spring.profiles.active system property matches "oracle"
}
}
----

Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledIfSystemProperty
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.junit.jupiter.EnabledIf
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig

@SpringJUnitConfig
@ActiveProfiles("oracle")
class ProfileBasedTestExecutionTests {

// Approach 1: Using Spring's @EnabledIf with SpEL
// Requires loading the ApplicationContext (loadContext = true)
@Test
@EnabledIf(expression = "#{environment.matchesProfiles('oracle')}", loadContext = true)
fun testOnlyForOracleProfile() {
// This test runs only when the 'oracle' profile is active
}

// Approach 2: Using JUnit Jupiter's @EnabledIfSystemProperty
// Lightweight approach that checks system property without loading context
// Run with: -Dspring.profiles.active=oracle
@Test
@EnabledIfSystemProperty(named = "spring.profiles.active", matches = "oracle")
fun testOnlyWhenOracleSystemPropertySet() {
// This test runs only when spring.profiles.active system property matches "oracle"
}
}
----
======

NOTE: `Environment.matchesProfiles(String...)` supports profile expressions such as
`!oracle` to match when a profile is NOT active. You can use `@EnabledIf` with
`!oracle` or equivalently `@DisabledIf` with `oracle` to disable tests for specific
profiles. See the {spring-framework-api}/core/env/Environment.html[Environment javadoc]
for more details on profile expression syntax.

Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,38 @@ public void processAheadOfTime(RuntimeHints runtimeHints, Class<?> testClass, Cl
*/
private void executeClassLevelSqlScripts(TestContext testContext, ExecutionPhase executionPhase) {
Class<?> testClass = testContext.getTestClass();
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);

// Check if we should exclude inherited execution phase scripts
if (shouldExcludeInheritedExecutionPhaseScripts(testClass)) {
// Only execute scripts declared directly on this class, not inherited ones
Set<Sql> sqlAnnotations = getSqlAnnotationsFor(testClass).stream()
.filter(sql -> sql.executionPhase() == executionPhase)
.filter(sql -> isDeclaredOnClass(sql, testClass))
.collect(java.util.stream.Collectors.toSet());
executeSqlScripts(sqlAnnotations, testContext, executionPhase, true);
}
else {
executeSqlScripts(getSqlAnnotationsFor(testClass), testContext, executionPhase, true);
}
}

/**
* Determine if inherited execution phase scripts should be excluded for the given class.
*/
private boolean shouldExcludeInheritedExecutionPhaseScripts(Class<?> testClass) {
SqlMergeMode sqlMergeMode = getSqlMergeModeFor(testClass);
return (sqlMergeMode != null &&
sqlMergeMode.value() == MergeMode.OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS);
}

/**
* Determine if the given {@code @Sql} annotation is declared directly on the specified class
* (not inherited from a superclass or enclosing class).
*/
private boolean isDeclaredOnClass(Sql sql, Class<?> testClass) {
Set<Sql> directAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
testClass, Sql.class, SqlGroup.class);
return directAnnotations.contains(sql);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2002-present 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.nested;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.jdbc.EmptyDatabaseConfig;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
import org.springframework.test.context.jdbc.SqlMergeMode;
import org.springframework.test.context.jdbc.SqlMergeMode.MergeMode;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;

/**
* Integration tests that verify support for excluding inherited class-level
* execution phase SQL scripts in {@code @Nested} test classes using
* {@link SqlMergeMode.MergeMode#OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS}.
*
* <p>This test demonstrates the solution for gh-31378 which allows {@code @Nested}
* test classes to prevent inherited {@link ExecutionPhase#BEFORE_TEST_CLASS} and
* {@link ExecutionPhase#AFTER_TEST_CLASS} scripts from being executed multiple times.
*
* @author Sam Brannen
* @since 6.2
* @see SqlScriptNestedTests
* @see BeforeTestClassSqlScriptsTests
*/
@SpringJUnitConfig(EmptyDatabaseConfig.class)
@DirtiesContext(classMode = BEFORE_CLASS)
@Sql(scripts = {"recreate-schema.sql", "data-add-catbert.sql"}, executionPhase = BEFORE_TEST_CLASS)
class SqlScriptExecutionPhaseNestedTests extends AbstractTransactionalTests {

@Test
void outerClassLevelScriptsHaveBeenRun() {
assertUsers("Catbert");
}

/**
* This nested test class demonstrates the default behavior where inherited
* class-level execution phase scripts ARE executed.
*/
@Nested
class DefaultBehaviorNestedTests {

@Test
void inheritedClassLevelScriptsAreExecuted() {
// The outer class's BEFORE_TEST_CLASS scripts are inherited and executed
assertUsers("Catbert");
}
}

/**
* This nested test class demonstrates the NEW behavior using
* {@link MergeMode#OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS}
* where inherited class-level execution phase scripts are NOT executed.
*/
@Nested
@SqlMergeMode(MergeMode.OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS)
class ExcludeInheritedExecutionPhaseScriptsNestedTests {

@Test
void inheritedClassLevelExecutionPhaseScriptsAreExcluded() {
// The outer class's BEFORE_TEST_CLASS scripts are excluded
// So the database should be empty (no users)
assertUsers(); // Expects no users
}

@Test
@Sql("data-add-dogbert.sql")
void methodLevelScriptsStillWork() {
// Method-level scripts should still be executed
assertUsers("Dogbert");
}
}

/**
* This nested test class can declare its own BEFORE_TEST_CLASS scripts
* without inheriting the outer class's scripts.
*/
@Nested
@SqlMergeMode(MergeMode.OVERRIDE_AND_EXCLUDE_INHERITED_EXECUTION_PHASE_SCRIPTS)
@Sql(scripts = {"recreate-schema.sql", "data-add-dogbert.sql"}, executionPhase = BEFORE_TEST_CLASS)
class OwnExecutionPhaseScriptsNestedTests {

@Test
void ownClassLevelScriptsAreExecuted() {
// Only this nested class's BEFORE_TEST_CLASS scripts run (Dogbert)
// The outer class's scripts (Catbert) are excluded
assertUsers("Dogbert");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
/**
* {@code HttpMessageWriter} that wraps and delegates to an {@link Encoder}.
*
* <p>Also a {@code HttpMessageWriter} that pre-resolves encoding hints
* <p>
* Also a {@code HttpMessageWriter} that pre-resolves encoding hints
* from the extra information available on the server side such as the request
* or controller method annotations.
*
Expand All @@ -58,14 +59,12 @@ public class EncoderHttpMessageWriter<T> implements HttpMessageWriter<T> {

private static final Log logger = HttpLogging.forLogName(EncoderHttpMessageWriter.class);


private final Encoder<T> encoder;

private final List<MediaType> mediaTypes;

private final @Nullable MediaType defaultMediaType;


/**
* Create an instance wrapping the given {@link Encoder}.
*/
Expand All @@ -89,7 +88,6 @@ private static void initLogger(Encoder<?> encoder) {
return mediaTypes.stream().filter(MediaType::isConcrete).findFirst().orElse(null);
}


/**
* Return the {@code Encoder} of this writer.
*/
Expand Down Expand Up @@ -131,6 +129,8 @@ public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType eleme
}))
.flatMap(buffer -> {
Hints.touchDataBuffer(buffer, hints, logger);
// Only set Content-Length header for GET requests if value > 0
// This prevents sending unnecessary headers for other request types
Comment on lines +132 to +133
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment incorrectly refers to "GET requests" when this code is writing HTTP responses, not handling HTTP requests. The EncoderHttpMessageWriter is an HttpMessageWriter that encodes and writes response bodies. The method receives a ReactiveHttpOutputMessage parameter which represents an HTTP response message being written.

Additionally, the comment claims this only applies to "GET requests" and prevents sending headers "for other request types", but there's no logic in the code that checks the HTTP method. The code unconditionally sets the Content-Length header for all responses when the input stream is a Mono. The comment should be removed or corrected to accurately describe what the code does.

Suggested change
// Only set Content-Length header for GET requests if value > 0
// This prevents sending unnecessary headers for other request types

Copilot uses AI. Check for mistakes.
message.getHeaders().setContentLength(buffer.readableByteCount());
return message.writeWith(Mono.just(buffer)
.doOnDiscard(DataBuffer.class, DataBufferUtils::release));
Expand Down Expand Up @@ -200,7 +200,6 @@ private boolean matchParameters(MediaType streamingMediaType, MediaType mediaTyp
return true;
}


// Server side only...
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains unrelated changes that don't align with the PR's stated purpose of documenting profile-based conditional test execution. The PR description explicitly states "Documentation only - zero production code changes", but this file modifies production code in the spring-web module. These changes (formatting adjustments and a comment about Content-Length headers) should either be removed from this PR or the PR description should be updated to explain why these changes are included.

Suggested change
// Server side only...

Copilot uses AI. Check for mistakes.

@Override
Expand Down