The purpose of the mergeMode attribute in the ContextCustomizerFactories annotation is to define how custom ContextCustomizerFactory implementations should interact with the default factories provided by the Spring Framework.
The ContextCustomizerFactories#mergeMode is of enum type and has two values:
- MERGE_WITH_DEFAULTS Indicates that locally declared factories should be merged with the default factories.
- REPLACE_DEFAULTS Indicates that locally declared factories should replace the default factories.
MERGE_WITH_DEFAULTS is the default, and we have seen the examples of it in the last two tutorials.
Why use REPLACE_DEFAULTS?
In most scenarios, you should use MERGE_WITH_DEFAULTS to keep standard features. However, REPLACE_DEFAULTS should only be used when you need full control over context customization and want to prevent Spring’s default ContextCustomizerFactory implementations from being registered.
When this mode is active, the following core Spring default factories are not registered:
BeanOverrideContextCustomizerFactory: Handles @MockitoBean, @TestBean.
DynamicPropertiesContextCustomizerFactory: Handles @DynamicPropertySource.
MockServerContainerContextCustomizerFactory:This factory creates customizers that set up a mock ServerContainer for WebSocket testing. It provides a testing environment for WebSocket functionality without requiring a full servlet container to be running.
Example
This example demonstrates the difference between MERGE_WITH_DEFAULTS and REPLACE_DEFAULTS using two test classes. Both tests attempt to inject a dynamic property value using @DynamicPropertySource, but only one succeeds. The CustomReplaceFactory simply prints a message when executed, allowing us to verify which factories are active. By comparing the behavior of both test classes side by side, you will clearly see how REPLACE_DEFAULTS disables Spring's default property handling while MERGE_WITH_DEFAULTS preserves it.
package com.logicbig.example;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import java.util.List;
public class CustomReplaceFactory implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(
Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
return (context, mergedConfig) -> {
System.out.println("Custom factory running.");
};
}
}
package com.logicbig.example;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
}
Using MERGE_WITH_DEFAULTS
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.ContextCustomizerFactories;
import org.springframework.test.context.ContextCustomizerFactories.MergeMode;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(MyConfig.class)
@ContextCustomizerFactories(
factories = CustomReplaceFactory.class,
mergeMode = MergeMode.MERGE_WITH_DEFAULTS
)
public class MergeWithDefaultsTest {
// Default value is used because the dynamic property will be missing
@Value("${dynamic.service.url:NOT_FOUND}")
private String serviceUrl;
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("dynamic.service.url", () -> "http://dynamic-host:8080");
}
@Test
void testMethod() {
System.out.println(serviceUrl);
}
}
Output$ mvn test -Dtest="MergeWithDefaultsTest" [INFO] Scanning for projects... [INFO] [INFO] --< com.logicbig.example:context-customizer-factories-replace-defaults >-- [INFO] Building context-customizer-factories-replace-defaults 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ context-customizer-factories-replace-defaults --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-core-testing\context-customizer-factories-replace-defaults\src\main\resources [INFO] [INFO] --- compiler:3.13.0:compile (default-compile) @ context-customizer-factories-replace-defaults --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ context-customizer-factories-replace-defaults --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-core-testing\context-customizer-factories-replace-defaults\src\test\resources [INFO] [INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ context-customizer-factories-replace-defaults --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ context-customizer-factories-replace-defaults --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.MergeWithDefaultsTest Custom factory running. http://dynamic-host:8080 [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.298 s -- in com.logicbig.example.MergeWithDefaultsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.786 s [INFO] Finished at: 2026-02-15T19:08:11+08:00 [INFO] ------------------------------------------------------------------------
Using REPLACE_DEFAULTS
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.ContextCustomizerFactories;
import org.springframework.test.context.ContextCustomizerFactories.MergeMode;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(MyConfig.class)
@ContextCustomizerFactories(
factories = CustomReplaceFactory.class,
mergeMode = MergeMode.REPLACE_DEFAULTS
)
public class ReplaceDefaultsTest {
// Default value is used because the dynamic property will be missing
@Value("${dynamic.service.url:NOT_FOUND}")
private String serviceUrl;
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("dynamic.service.url", () -> "http://dynamic-host:8080");
}
@Test
void testMethod() {
System.out.println(serviceUrl);
}
}
Output$ mvn test -Dtest="ReplaceDefaultsTest" [INFO] Scanning for projects... [INFO] [INFO] --< com.logicbig.example:context-customizer-factories-replace-defaults >-- [INFO] Building context-customizer-factories-replace-defaults 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ context-customizer-factories-replace-defaults --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-core-testing\context-customizer-factories-replace-defaults\src\main\resources [INFO] [INFO] --- compiler:3.13.0:compile (default-compile) @ context-customizer-factories-replace-defaults --- [INFO] No sources to compile [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ context-customizer-factories-replace-defaults --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-core-testing\context-customizer-factories-replace-defaults\src\test\resources [INFO] [INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ context-customizer-factories-replace-defaults --- [INFO] Recompiling the module because of changed source code. [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 4 source files with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ context-customizer-factories-replace-defaults --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ReplaceDefaultsTest Custom factory running. NOT_FOUND [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.283 s -- in com.logicbig.example.ReplaceDefaultsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.690 s [INFO] Finished at: 2026-02-15T19:06:59+08:00 [INFO] ------------------------------------------------------------------------
Conclusion
The REPLACE_DEFAULTS merge mode provides fine-grained control over Spring's test context bootstrapping by completely replacing default ContextCustomizerFactory implementations. As demonstrated in the examples, when REPLACE_DEFAULTS is used, Spring's default factories that handle annotations like @DynamicPropertySource are bypassed, causing the dynamic property to be unavailable and the fallback value to be used. In contrast, MERGE_WITH_DEFAULTS preserves these default factories, allowing standard annotations to function normally. This feature is particularly valuable for framework developers and advanced users who need to implement custom testing semantics, enhance security by disabling specific default behaviors, or optimize test performance in large test suites.
Example ProjectDependencies and Technologies Used: - spring-test 7.0.4 (Spring TestContext Framework)
- spring-context 7.0.4 (Spring Context)
Version Compatibility: 6.1.0 - 7.0.4 Version compatibilities of spring-context with this example:
- 6.1.0
- 6.1.1
- 6.1.2
- 6.1.3
- 6.1.4
- 6.1.5
- 6.1.6
- 6.1.7
- 6.1.8
- 6.1.9
- 6.1.10
- 6.1.11
- 6.1.12
- 6.1.13
- 6.1.14
- 6.1.15
- 6.1.16
- 6.1.17
- 6.1.18
- 6.1.19
- 6.1.20
- 6.1.21
- 6.2.0
- 6.2.1
- 6.2.2
- 6.2.3
- 6.2.4
- 6.2.5
- 6.2.6
- 6.2.7
- 6.2.8
- 6.2.9
- 6.2.10
- 6.2.11
- 6.2.12
- 6.2.13
- 6.2.14
- 6.2.15
- 6.2.16
- 7.0.0
- 7.0.1
- 7.0.2
- 7.0.3
- 7.0.4
Versions in green have been tested.
- junit-jupiter-engine 6.0.2 (Module "junit-jupiter-engine" of JUnit)
- JDK 25
- Maven 3.9.11
|