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.
====

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 suggests this is specific to GET requests, but the code unconditionally sets Content-Length for all request types. The comment is misleading because:

  1. This method doesn't have access to the HTTP request method
  2. Content-Length is set regardless of request type
  3. The comment contradicts the actual behavior

Either remove this comment or clarify that Content-Length is set for all requests when processing Mono publishers.

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

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...

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ExpressionValueMethodArgumentResolverTests {
private MethodParameter paramSystemProperty;
private MethodParameter paramNotSupported;
private MethodParameter paramAlsoNotSupported;
private MethodParameter paramCustomProperty;


@BeforeEach
Expand All @@ -61,6 +62,7 @@ void setup() throws Exception {
this.paramSystemProperty = new MethodParameter(method, 0);
this.paramNotSupported = new MethodParameter(method, 1);
this.paramAlsoNotSupported = new MethodParameter(method, 2);
this.paramCustomProperty = new MethodParameter(method, 3);
}


Expand Down Expand Up @@ -93,14 +95,32 @@ void resolveSystemProperty() {

}

// TODO: test with expression for ServerWebExchange
@Test
void resolveWithServerWebExchange() {
System.setProperty("customProperty", "42");
try {
// Configure the exchange
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

Mono<Object> mono = this.resolver.resolveArgument(
this.paramCustomProperty, new BindingContext(), exchange);

Object value = mono.block();
assertThat(value).isEqualTo(42);
}
finally {
System.clearProperty("customProperty");
}
}
Comment on lines +98 to +115
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 test doesn't adequately test ServerWebExchange-specific functionality. It simply creates a new MockServerWebExchange and resolves a system property expression, which is identical to the existing resolveSystemProperty() test. The TODO suggested testing "expression for ServerWebExchange", which likely meant testing expressions that access ServerWebExchange attributes or properties (e.g., "#{exchange.request.uri}" or similar), not just passing a different exchange instance with the same system property expression.

Copilot uses AI. Check for mistakes.


@SuppressWarnings("unused")
public void params(
@Value("#{systemProperties.systemProperty}") int param1,
String notSupported,
@Value("#{systemProperties.foo}") Mono<String> alsoNotSupported) {
@Value("#{systemProperties.foo}") Mono<String> alsoNotSupported,
@Value("#{systemProperties.customProperty}") int param4) {
}

}