Close

Spring Core Testing - Annotation Based ContextCustomizers

[Last Updated: Feb 6, 2026]

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:

  1. Custom Annotation: Marks test classes that need customization
  2. ContextCustomizerFactory: Detects annotations and creates customizers
  3. 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.factories

org.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"));
    }
}

Output

D:\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:

  1. Spring discovers our factory through spring.factories
  2. The factory checks for @EnableGreetingServiceMock annotation
  3. If found, it creates a GreetingServiceContextCustomizer
  4. The customizer removes the real bean and registers the mock bean
  5. 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 Project

Dependencies and Technologies Used:

  • spring-context 7.0.3 (Spring Context)
     Version Compatibility: 6.1.0 - 7.0.3Version List
    ×

    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

Spring Core Testing - Context Customizers Select All Download
  • context-customizers-with-custom-annotation
    • src
      • main
        • java
          • com
            • logicbig
              • example
      • test
        • java
          • com
            • logicbig
              • example
                • GreetingServiceContextCustomizer.java
          • resources
            • META-INF

    See Also

    Join