or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

assertj-integration.mdcontext-runners.mdindex.mdintegration-testing.mdjson-testing.mdoutput-capture.mdtest-configuration.mdtest-properties.mdweb-test-utilities.md
tile.json

test-configuration.mddocs/

Test Configuration and Components

Define test-specific bean configurations and components that don't interfere with production configuration detection.

Package

org.springframework.boot.test.context

Core Imports

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.context.TestComponent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

@TestConfiguration

Full Package: org.springframework.boot.test.context.TestConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@TestComponent
public @interface TestConfiguration {
    String value() default "";                    // Bean name (optional)
    boolean proxyBeanMethods() default true;      // CGLIB proxying
}

Key Differences from @Configuration:

  • Does NOT prevent @SpringBootConfiguration detection
  • Typically used as static inner class in tests
  • Can be imported explicitly with @Import

Pattern: Static Inner Test Configuration

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;

@SpringBootTest
class ServiceTest {

    @TestConfiguration
    static class Config {
        @Bean
        @Primary  // Overrides production bean
        public DataSource testDataSource() {
            return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
        }
    }

    @Autowired
    private DataSource dataSource;

    @Test
    void testUsesTestDataSource() {
        // Uses test configuration
    }
}

Pattern: Disable Proxy Methods for Performance

@TestConfiguration(proxyBeanMethods = false)
static class FastConfig {
    // No CGLIB proxying - faster startup
    // Bean method calls create new instances
}

@TestComponent

Full Package: org.springframework.boot.test.context.TestComponent

Marks components that should only be picked up during testing.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface TestComponent {
    String value() default "";
}

Pattern: Test-Only Component

import org.springframework.boot.test.context.TestComponent;

@TestComponent
public class TestDataGenerator {
    public List<TestData> generateData() {
        return List.of(new TestData("test1"), new TestData("test2"));
    }
}

Common Patterns

Pattern: Shared Test Configuration

// SharedTestConfig.java
@TestConfiguration
public class SharedTestConfig {
    @Bean
    public Clock fixedClock() {
        return Clock.fixed(Instant.parse("2024-01-01T00:00:00Z"), ZoneId.of("UTC"));
    }
}

// Test classes
@SpringBootTest
@Import(SharedTestConfig.class)
class Test1 {
    @Autowired Clock clock;
}

@SpringBootTest
@Import(SharedTestConfig.class)
class Test2 {
    @Autowired Clock clock;
}

Pattern: Conditional Test Beans

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

@SpringBootTest
class ConditionalTestConfigTest {

    @TestConfiguration
    static class TestConfig {

        @Bean
        @ConditionalOnProperty(name = "test.feature.enabled", havingValue = "true")
        public FeatureService featureService() {
            return new TestFeatureService();
        }

        @Bean
        @ConditionalOnProperty(name = "test.feature.enabled", havingValue = "false", matchIfMissing = true)
        public FeatureService disabledFeatureService() {
            return new DisabledFeatureService();
        }
    }

    @Autowired
    private FeatureService featureService;

    @Test
    void testConditionalBean() {
        assertThat(featureService).isNotNull();
    }
}

Pattern: Multiple Test Configurations

// DatabaseTestConfig.java
@TestConfiguration
public class DatabaseTestConfig {
    @Bean
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("test-schema.sql")
            .addScript("test-data.sql")
            .build();
    }
}

// SecurityTestConfig.java
@TestConfiguration
public class SecurityTestConfig {
    @Bean
    public SecurityService testSecurityService() {
        return new TestSecurityService();
    }
}

// Test class
@SpringBootTest
@Import({DatabaseTestConfig.class, SecurityTestConfig.class})
class MultipleTestConfigsTest {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private SecurityService securityService;

    @Test
    void testMultipleConfigs() {
        assertThat(dataSource).isNotNull();
        assertThat(securityService).isNotNull();
    }
}

Pattern: Test Configuration with Property Sources

import org.springframework.test.context.TestPropertySource;
import org.springframework.beans.factory.annotation.Value;

@SpringBootTest
class PropertySourceTestConfigTest {

    @TestConfiguration
    @TestPropertySource(properties = {
        "test.mode=integration",
        "test.timeout=5000"
    })
    static class TestConfig {
        @Bean
        public ModeService modeService(@Value("${test.mode}") String mode) {
            return new ModeService(mode);
        }

        @Bean
        public TimeoutService timeoutService(@Value("${test.timeout}") int timeout) {
            return new TimeoutService(timeout);
        }
    }

    @Autowired
    private ModeService modeService;

    @Autowired
    private TimeoutService timeoutService;

    @Test
    void testPropertyInjection() {
        assertThat(modeService.getMode()).isEqualTo("integration");
        assertThat(timeoutService.getTimeout()).isEqualTo(5000);
    }
}

Pattern: Test Fixtures Configuration

@SpringBootTest
class TestFixturesTest {

    @TestConfiguration
    static class FixtureConfig {
        @Bean
        public TestFixtureLoader fixtureLoader() {
            TestFixtureLoader loader = new TestFixtureLoader();
            loader.loadFixture("users.json");
            loader.loadFixture("products.json");
            loader.loadFixture("orders.json");
            return loader;
        }

        @Bean
        public TestDataCleaner dataCleaner() {
            return new TestDataCleaner();
        }
    }

    @Autowired
    private TestFixtureLoader fixtureLoader;

    @Autowired
    private TestDataCleaner dataCleaner;

    @Test
    void testWithFixtures() {
        assertThat(fixtureLoader.getLoadedFixtures())
            .hasSize(3)
            .contains("users.json", "products.json", "orders.json");
    }

    @AfterEach
    void cleanup() {
        dataCleaner.cleanTestData();
    }
}

Pattern: Combining with @MockBean

import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.Mockito.*;

@SpringBootTest
class MockBeanWithTestConfigTest {

    @TestConfiguration
    static class TestConfig {
        @Bean
        public ServiceA serviceA(ExternalService externalService) {
            return new ServiceA(externalService);
        }

        @Bean
        public ServiceB serviceB(ExternalService externalService) {
            return new ServiceB(externalService);
        }
    }

    @MockBean
    private ExternalService externalService;

    @Autowired
    private ServiceA serviceA;

    @Autowired
    private ServiceB serviceB;

    @Test
    void testWithMockAndTestConfig() {
        when(externalService.call()).thenReturn("mocked-response");

        assertThat(serviceA.process()).contains("mocked-response");
        assertThat(serviceB.process()).contains("mocked-response");

        verify(externalService, times(2)).call();
    }
}

Pattern: Test Configuration for Context Runners

import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class ContextRunnerWithTestConfigTest {

    @TestConfiguration
    static class TestConfig {
        @Bean
        public MyService myService() {
            return new MyService("test-mode");
        }
    }

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withUserConfiguration(TestConfig.class);

    @Test
    void testWithTestConfig() {
        contextRunner.run(context -> {
            assertThat(context).hasSingleBean(MyService.class);
            assertThat(context.getBean(MyService.class).getMode())
                .isEqualTo("test-mode");
        });
    }
}

Pattern: Override Production Beans

@SpringBootTest
class BeanOverrideTest {

    @TestConfiguration
    static class TestConfig {
        @Bean
        @Primary  // Overrides production bean
        public EmailService emailService() {
            return new MockEmailService(); // Doesn't send real emails
        }

        @Bean
        @Primary
        public PaymentGateway paymentGateway() {
            return new MockPaymentGateway(); // Doesn't charge real payments
        }
    }

    @Autowired
    private EmailService emailService;

    @Autowired
    private PaymentGateway paymentGateway;

    @Test
    void testWithMockedExternalServices() {
        emailService.sendEmail("test@example.com", "Test");
        assertThat(((MockEmailService) emailService).getSentEmails()).hasSize(1);

        paymentGateway.charge(100.00);
        assertThat(((MockPaymentGateway) paymentGateway).getCharges()).hasSize(1);
    }
}

Pattern: Environment-Specific Configuration

@SpringBootTest
@ActiveProfiles("test")
class EnvironmentSpecificTestConfigTest {

    @TestConfiguration
    @Profile("test")
    static class TestProfileConfig {
        @Bean
        public DatabaseInitializer databaseInitializer() {
            return new TestDatabaseInitializer();
        }
    }

    @Autowired
    private DatabaseInitializer initializer;

    @Test
    void testProfileSpecificBean() {
        assertThat(initializer).isInstanceOf(TestDatabaseInitializer.class);
    }
}

Pattern: Nested Test Configurations

@SpringBootTest
class NestedTestConfigTest {

    @TestConfiguration
    static class ParentConfig {
        @Bean
        public ServiceA serviceA() {
            return new ServiceA();
        }
    }

    @TestConfiguration
    @Import(ParentConfig.class)
    static class ChildConfig {
        @Bean
        public ServiceB serviceB(ServiceA serviceA) {
            return new ServiceB(serviceA);
        }
    }

    @Autowired
    private ServiceA serviceA;

    @Autowired
    private ServiceB serviceB;

    @Test
    void testNestedConfig() {
        assertThat(serviceA).isNotNull();
        assertThat(serviceB).isNotNull();
        assertThat(serviceB.getServiceA()).isSameAs(serviceA);
    }
}

@TestComponent Usage Patterns

Pattern: Test-Only Repository

import org.springframework.boot.test.context.TestComponent;
import org.springframework.stereotype.Repository;

@TestComponent
@Repository
public class TestDataRepository {

    private final Map<String, TestData> storage = new ConcurrentHashMap<>();

    public TestData save(TestData data) {
        storage.put(data.getId(), data);
        return data;
    }

    public Optional<TestData> findById(String id) {
        return Optional.ofNullable(storage.get(id));
    }

    public List<TestData> findAll() {
        return new ArrayList<>(storage.values());
    }

    public void deleteAll() {
        storage.clear();
    }
}

@SpringBootTest
class TestComponentTest {

    @Autowired
    private TestDataRepository repository;

    @Test
    void testInMemoryRepository() {
        TestData data = new TestData("test-1", "value");
        repository.save(data);

        assertThat(repository.findById("test-1"))
            .isPresent()
            .get()
            .extracting("value")
            .isEqualTo("value");
    }

    @AfterEach
    void cleanup() {
        repository.deleteAll();
    }
}

Pattern: Test Data Generator Component

@TestComponent
public class TestDataGenerator {

    private final Random random = new Random(42); // Fixed seed for reproducibility

    public User generateUser() {
        return new User(
            "user-" + random.nextInt(1000),
            "test" + random.nextInt(100) + "@example.com"
        );
    }

    public List<User> generateUsers(int count) {
        return IntStream.range(0, count)
            .mapToObj(i -> generateUser())
            .collect(Collectors.toList());
    }

    public Product generateProduct() {
        return new Product(
            "Product-" + random.nextInt(1000),
            random.nextDouble() * 100
        );
    }
}

@SpringBootTest
class DataGeneratorTest {

    @Autowired
    private TestDataGenerator generator;

    @Test
    void testDataGeneration() {
        List<User> users = generator.generateUsers(5);
        assertThat(users).hasSize(5);
        assertThat(users).allMatch(u -> u.getEmail().contains("@example.com"));
    }
}

Pattern: Test Event Listener

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

@TestComponent
public class TestEventCollector {

    private final List<Object> capturedEvents = new ArrayList<>();

    @EventListener
    public void onApplicationEvent(Object event) {
        capturedEvents.add(event);
    }

    public List<Object> getCapturedEvents() {
        return new ArrayList<>(capturedEvents);
    }

    public <T> List<T> getCapturedEventsOfType(Class<T> type) {
        return capturedEvents.stream()
            .filter(type::isInstance)
            .map(type::cast)
            .collect(Collectors.toList());
    }

    public void clear() {
        capturedEvents.clear();
    }
}

@SpringBootTest
class EventTest {

    @Autowired
    private TestEventCollector eventCollector;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Test
    void testEventCapture() {
        UserCreatedEvent event = new UserCreatedEvent("user-1");
        eventPublisher.publishEvent(event);

        assertThat(eventCollector.getCapturedEventsOfType(UserCreatedEvent.class))
            .hasSize(1)
            .first()
            .extracting("userId")
            .isEqualTo("user-1");
    }

    @AfterEach
    void cleanup() {
        eventCollector.clear();
    }
}

FilteredClassLoader

Full Package: org.springframework.boot.test.context.FilteredClassLoader Since: 2.0.0

Test classloader that filters specific classes, packages, or resources to simulate missing dependencies or test classloading scenarios.

/**
 * Test URLClassLoader that filters classes and resources
 * Useful for testing auto-configuration conditional behavior
 * @since 2.0.0
 */
public class FilteredClassLoader extends URLClassLoader implements SmartClassLoader {

    /**
     * Create FilteredClassLoader hiding specific classes
     * @param hiddenClasses classes to hide from classpath
     */
    public FilteredClassLoader(Class<?>... hiddenClasses);

    /**
     * Create FilteredClassLoader with parent hiding specific classes
     * @param parent parent classloader
     * @param hiddenClasses classes to hide
     */
    public FilteredClassLoader(ClassLoader parent, Class<?>... hiddenClasses);

    /**
     * Create FilteredClassLoader hiding entire packages
     * @param hiddenPackages packages to hide (e.g., "com.example")
     */
    public FilteredClassLoader(String... hiddenPackages);

    /**
     * Create FilteredClassLoader hiding resources
     * @param hiddenResources classpath resources to hide
     * @since 2.1.0
     */
    public FilteredClassLoader(ClassPathResource... hiddenResources);

    /**
     * Create FilteredClassLoader with custom predicates
     * @param filters predicates returning true to hide class/resource
     */
    @SafeVarargs
    public FilteredClassLoader(Predicate<String>... filters);

    /**
     * Define a class that passes filters
     * @param name class name
     * @param b class bytes
     * @param protectionDomain protection domain
     * @return defined class
     */
    public Class<?> publicDefineClass(String name, byte[] b, ProtectionDomain protectionDomain);

    /**
     * Filter for specific classes
     */
    public static final class ClassFilter implements Predicate<String> {
        public static ClassFilter of(Class<?>... hiddenClasses);
        public boolean test(String className);
    }

    /**
     * Filter for packages
     */
    public static final class PackageFilter implements Predicate<String> {
        public static PackageFilter of(String... hiddenPackages);
        public boolean test(String className);
    }

    /**
     * Filter for resources
     * @since 2.1.0
     */
    public static final class ClassPathResourceFilter implements Predicate<String> {
        public static ClassPathResourceFilter of(ClassPathResource... hiddenResources);
        public boolean test(String resourceName);
    }
}

Pattern: Test Missing Dependency

import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class ConditionalAutoConfigTest {

    @Test
    void whenJacksonMissing_autoConfigDisabled() {
        new ApplicationContextRunner()
            .withClassLoader(new FilteredClassLoader(
                com.fasterxml.jackson.databind.ObjectMapper.class))
            .withUserConfiguration(MyAutoConfiguration.class)
            .run(context -> {
                assertThat(context).doesNotHaveBean(JacksonFeature.class);
            });
    }
}

Pattern: Test Missing Package

@Test
void whenSpringSecurityMissing_securityDisabled() {
    new ApplicationContextRunner()
        .withClassLoader(new FilteredClassLoader("org.springframework.security"))
        .withUserConfiguration(SecurityAutoConfig.class)
        .run(context -> {
            assertThat(context).doesNotHaveBean(SecurityFilterChain.class);
        });
}

Pattern: Test Missing Resource

import org.springframework.core.io.ClassPathResource;

@Test
void whenConfigFileMissing_usesDefaults() {
    new ApplicationContextRunner()
        .withClassLoader(new FilteredClassLoader(
            new ClassPathResource("config/custom-settings.properties")))
        .withUserConfiguration(ConfigAutoConfiguration.class)
        .run(context -> {
            assertThat(context.getBean(Settings.class).isDefault()).isTrue();
        });
}

Pattern: Test Multiple Class Filters

@Test
void whenMultipleDependenciesMissing_fallbackEnabled() {
    new ApplicationContextRunner()
        .withClassLoader(new FilteredClassLoader(
            org.elasticsearch.client.RestClient.class,
            org.apache.kafka.clients.producer.KafkaProducer.class))
        .withUserConfiguration(DataSinkAutoConfiguration.class)
        .run(context -> {
            assertThat(context).hasSingleBean(InMemoryDataSink.class);
            assertThat(context).doesNotHaveBean(ElasticsearchSink.class);
            assertThat(context).doesNotHaveBean(KafkaSink.class);
        });
}

Pattern: Custom Predicate Filter

@Test
void whenTestClassesFiltered_productionOnlyLoads() {
    Predicate<String> testFilter = className ->
        className.contains(".test.") || className.endsWith("Test");

    new ApplicationContextRunner()
        .withClassLoader(new FilteredClassLoader(testFilter))
        .withUserConfiguration(MixedConfiguration.class)
        .run(context -> {
            assertThat(context).doesNotHaveBean(TestHelper.class);
            assertThat(context).hasSingleBean(ProductionService.class);
        });
}

Troubleshooting

Issue: Test configuration beans not found

  • Cause: Missing @Import for standalone @TestConfiguration
  • Solution: Add @Import(TestConfig.class) or use static inner class

Issue: Production bean not overridden

  • Cause: Missing @Primary annotation
  • Solution: Add @Primary to test bean

Issue: FilteredClassLoader not working with context runner

  • Cause: Missing withClassLoader() call
  • Solution: Add .withClassLoader(new FilteredClassLoader(...)) to runner chain

Issue: Class still loads despite filter

  • Cause: Class already loaded by parent classloader
  • Solution: Ensure test runs with fresh JVM or use proper test isolation

See Also

  • Integration Testing - @SpringBootTest usage
  • Context Runners - Using test config with runners