ContextCustomizer (last tutorial) is a powerful feature in Spring's testing framework that allows you to dynamically modify the ApplicationContext before it's created for your tests. This provides fine-grained control over your test environment without changing production configurations.
This example shows annotation based ContextCustomizer pattern which involves three main components:
- Custom Annotation: Marks test classes that need customization
- ContextCustomizerFactory: Detects annotations and creates customizers
- ContextCustomizer: Performs the actual context modifications
Example
Let's create an example where we use a custom annotation @EnableGreetingServiceMock to replace a real service with a mock during testing.
Service Interface and Implementation
On application site we have a simple greeting service.
package com.logicbig.example;
public interface GreetingService {
String getGreeting(String name);
}
package com.logicbig.example;
import org.springframework.stereotype.Component;
@Component
public class RealGreetingService implements GreetingService {
@Override
public String getGreeting(String name) {
return "Hello, " + name + "! (Real Service)";
}
}
package com.logicbig.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class AppConfig {
}
Test Folder
Custom Annotation
package com.logicbig.example;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface EnableGreetingServiceMock {
String message() default "Hello";
}
Service Interface Mock Implementation
package com.logicbig.example;
public class MockGreetingService implements GreetingService {
private final String prefix;
public MockGreetingService(String prefix) {
this.prefix = prefix;
}
@Override
public String getGreeting(String name) {
return prefix + ", " + name + "! (Mock Service)";
}
}
ContextCustomizer Implementation
This is where the magic happens. The customizer swaps the real bean with the mock:
package com.logicbig.example;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import java.util.Objects;
public class GreetingServiceContextCustomizer implements ContextCustomizer {
private final String message;
public GreetingServiceContextCustomizer(String message) {
this.message = message;
}
@Override
public void customizeContext(ConfigurableApplicationContext context,
MergedContextConfiguration mergedConfig) {
// Remove existing GreetingService bean
if (context.getBeanFactory().containsBean("greetingService")) {
context.getBeanFactory()
.destroyBean("greetingService");
}
// Register mock bean
MockGreetingService mockService = new MockGreetingService(message);
context.getBeanFactory().registerSingleton("greetingService", mockService);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
GreetingServiceContextCustomizer that = (GreetingServiceContextCustomizer) obj;
return Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hashCode(message);
}
}
ContextCustomizerFactory
The factory detects our annotation and creates the appropriate customizer:
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 GreetingServiceContextCustomizerFactory
implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(
Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
EnableGreetingServiceMock annotation =
testClass.getAnnotation(EnableGreetingServiceMock.class);
if (annotation == null) {
return null;
}
return new GreetingServiceContextCustomizer(
annotation.message());
}
}
Registering customizer factor
src/test/resources/META-INF/spring.factoriesorg.springframework.test.context.ContextCustomizerFactory=com.logicbig.example.GreetingServiceContextCustomizerFactory
Testing with ContextCustomizer
Now let's see how tests can use our custom annotation to enable mocking
Test Without Customizer
This test uses the real greeting service:
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.*;
@SpringJUnitConfig(AppConfig.class)
public class WithoutCustomizerTest {
@Autowired
private GreetingService greetingService;
@Test
public void testRealService() {
String result = greetingService.getGreeting("John");
System.out.println("Test 1 - Real Service: " + result);
assertTrue(result.contains("Real Service"));
}
}
Output$ mvn test -Dtest=WithoutCustomizerTest.java [INFO] Scanning for projects... [INFO] [INFO] --< com.logicbig.example:context-customizers-with-custom-annotation >--- [INFO] Building context-customizers-with-custom-annotation 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ context-customizers-with-custom-annotation --- [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-customizers-with-custom-annotation\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ context-customizers-with-custom-annotation --- [INFO] Changes detected - recompiling the module! :input tree [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 3 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ context-customizers-with-custom-annotation --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] Copying 1 resource from src\test\resources to target\test-classes [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ context-customizers-with-custom-annotation --- [INFO] Changes detected - recompiling the module! :dependency [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [INFO] Compiling 7 source files with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ context-customizers-with-custom-annotation --- [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.WithoutCustomizerTest Test 1 - Real Service: Hello, John! (Real Service) [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.317 s -- in com.logicbig.example.WithoutCustomizerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.550 s [INFO] Finished at: 2026-02-05T21:35:51+08:00 [INFO] ------------------------------------------------------------------------
Test With Custom Annotation
This test uses the mock greeting service with default message:
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.*;
@SpringJUnitConfig(AppConfig.class)
@EnableGreetingServiceMock
public class WithCustomizerTest {
@Autowired
private GreetingService greetingService;
@Test
public void testMockService() {
String result = greetingService.getGreeting("John");
System.out.println("Test 2 - Mock Service: " + result);
assertTrue(result.contains("Mock Service"));
assertTrue(result.contains("Hello"));
}
}
OutputD:\example-projects\spring-core-testing\context-customizers-with-custom-annotation>mvn test -Dtest=WithCustomizerTest.java [INFO] Scanning for projects... [INFO] [INFO] --< com.logicbig.example:context-customizers-with-custom-annotation >--- [INFO] Building context-customizers-with-custom-annotation 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ context-customizers-with-custom-annotation --- [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-customizers-with-custom-annotation\src\main\resources [INFO] [INFO] --- compiler:3.11.0:compile (default-compile) @ context-customizers-with-custom-annotation --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ context-customizers-with-custom-annotation --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] Copying 1 resource from src\test\resources to target\test-classes [INFO] [INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ context-customizers-with-custom-annotation --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ context-customizers-with-custom-annotation --- [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.WithCustomizerTest Test 2 - Mock Service: Hello, John! (Mock Service) [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.416 s -- in com.logicbig.example.WithCustomizerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.333 s [INFO] Finished at: 2026-02-05T21:37:18+08:00 [INFO] ------------------------------------------------------------------------
How It Works
When Spring loads the test context:
- Spring discovers our factory through
spring.factories
- The factory checks for
@EnableGreetingServiceMock annotation
- If found, it creates a
GreetingServiceContextCustomizer
- The customizer removes the real bean and registers the mock bean
- The test receives the mock implementation instead of the real one
Conclusion
The output shows that tests without the @EnableGreetingServiceMock annotation use the real service ("Real Service" in output), while tests with the annotation use the mock service ("Mock Service" in output). This demonstrates how ContextCustomizer allows dynamic context modification based on test class annotations, enabling flexible and reusable test configurations without modifying production code.
Example ProjectDependencies and Technologies Used: - spring-context 7.0.3 (Spring Context)
Version Compatibility: 6.1.0 - 7.0.3 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
- 7.0.0
- 7.0.1
- 7.0.2
- 7.0.3
Versions in green have been tested.
- spring-test 7.0.3 (Spring TestContext Framework)
- junit-jupiter 6.0.0 (Module "junit-jupiter" of JUnit)
- JDK 25
- Maven 3.9.11
|