@TestConfiguration defines beans used by a Spring Boot test while keeping the application’s primary configuration. It is useful when a test needs a deterministic collaborator, such as a fixed Clock, without changing production code. A nested test configuration is picked up for its test class; a top-level test configuration must be imported where it is needed. The annotation adds bean definitions. It does not, by itself, replace an existing bean that has the same type or name.
1. How Test Configuration Fits into a Boot Test
A SpringBootTest normally finds the application’s SpringBootApplication class by searching upward from the test package. A nested @TestConfiguration class adds its beans to that application context. A nested ordinary @Configuration class behaves differently: it can become the test’s primary configuration instead of adding to the discovered one. The Spring Boot testing reference explains this distinction.
@SpringBootTest
class InvoiceDueDateTest {
@TestConfiguration(proxyBeanMethods = false)
static class TestClockConfiguration {
@Bean Clock fixedClock() { /* ... */ }
}
}
A top-level @TestConfiguration class is excluded from component scanning so it does not affect unrelated tests. The test that needs it uses @Import explicitly. This makes a reusable configuration’s scope visible at the test class:
@SpringBootTest
@Import(FixedDateTestConfiguration.class)
class ImportedTestConfigurationTest {
// Uses the imported test beans.
}
Use the nested form for a bean needed by one test class. Use a top-level class when several test classes need the same setup. Neither form requires enabling bean-definition overriding for the example below.
2. An Invoice Due-Date Feature
An invoicing application gives customers 14 days to pay. InvoiceDueDateService calculates the due date from the current date. Reading the system clock directly inside the service would make a test depend on the day it runs, so the service receives a Clock through its constructor.
Production configuration supplies a UTC system clock. The test can then supply a fixed clock while leaving the real application service in place:
@SpringBootApplication
public class InvoiceApplication {
public static void main(String[] args) {
SpringApplication.run(InvoiceApplication.class, args);
}
@Bean
Clock systemClock() {
return Clock.systemUTC();
}
}
The service turns the injected time into a date and adds 14 days. There is no test-specific branch in this production class:
@Service
public class InvoiceDueDateService {
private final Clock clock;
public InvoiceDueDateService(Clock clock) {
this.clock = clock;
}
public LocalDate dueDate() {
return LocalDate.now(clock).plusDays(14);
}
}
The Maven project uses the Spring Boot 4.1.2 parent, Java 17, the base starter and the test starter. Those dependencies are sufficient for this example because the feature does not use a web server or database:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3. Use a Nested Test Configuration
The first test fixes the clock at 28 October 2026 UTC. A due date 14 days later is 11 November 2026, regardless of when Maven runs. The nested FixedClockConfiguration is discovered for this test along with InvoiceApplication.
@SpringBootTest
class NestedTestConfigurationTest {
@Autowired InvoiceDueDateService service;
@TestConfiguration(proxyBeanMethods = false)
static class FixedClockConfiguration {
@Bean @Primary
Clock fixedClock() {
return Clock.fixed(
Instant.parse("2026-10-28T00:00:00Z"), ZoneOffset.UTC);
}
}
@Test
void calculatesDueDateFromFixedClock() {
assertThat(service.dueDate())
.isEqualTo(LocalDate.of(2026, 11, 11));
}
}
The context contains both systemClock and fixedClock. @Primary tells Spring which Clock to inject into InvoiceDueDateService when the constructor asks for one without a qualifier. The test therefore exercises the real service with a predictable clock. proxyBeanMethods = false is appropriate here because this test bean method does not call another bean method in the configuration class.
4. Import a Reusable Test Configuration
A second test wants a fixed date of 1 January 2027. A top-level configuration can be shared by tests that need that same date, but Spring Boot does not scan it into every application test. The class lives under src/test/java and declares its test bean:
@TestConfiguration(proxyBeanMethods = false)
public class FixedDateTestConfiguration {
@Bean @Primary
Clock fixedClock() {
return Clock.fixed(
Instant.parse("2027-01-01T00:00:00Z"), ZoneOffset.UTC);
}
}
ImportedTestConfigurationTest names the class in @Import. The date assertion shows that its fixed clock reached the production service; if the system clock were injected instead, the assertion would depend on the real date and fail.
@SpringBootTest
@Import(FixedDateTestConfiguration.class)
class ImportedTestConfigurationTest {
@Autowired InvoiceDueDateService service;
@Test
void calculatesDueDateFromImportedClock() {
assertThat(service.dueDate())
.isEqualTo(LocalDate.of(2027, 1, 15));
}
}
These are two separate test contexts because their configuration differs. A top-level test configuration stays out of another test unless it is explicitly imported. This is useful when a shared test utility would otherwise alter tests that expect production wiring.
5. Adding a Bean Is Not the Same as Overriding One
Both tests add a second bean of type Clock. They do not redefine systemClock by name. Without @Primary or a qualifier, constructor injection by type would be ambiguous. Giving a test bean the same name as an existing production bean instead can raise a bean-definition override error; turning on global override permission merely to make a test pass obscures which bean is active.
@Bean @Primary
Clock fixedClock() {
return Clock.fixed(Instant.parse("2027-01-01T00:00:00Z"), ZoneOffset.UTC);
}
The fixed clock is a simple test implementation that gives a stable result. If the collaborator is an interface whose interactions or varying responses matter, @MockitoBean may fit better. See the Spring Boot MockitoBean guide for that testing boundary. Do not add a mock when a fixed value already makes the test deterministic.
6. Run the Example
Run the companion Maven module with Java 17 or later. Both tests should pass, showing the nested and imported clock choices independently:
mvn test
The verified run used Spring Boot 4.1.2 on Java 25 with a Java 17 target. Both test classes found InvoiceApplication as their primary configuration and completed their date assertions.
7. FAQs
7.1. Should I use @Configuration or @TestConfiguration inside a SpringBootTest?
Use @TestConfiguration when you want to add test beans to the application configuration that Spring Boot discovers. A nested ordinary @Configuration class can be treated as the primary configuration instead. That difference can leave expected application beans out of the test.
7.2. Does a top-level @TestConfiguration apply to every test?
No. Spring Boot excludes top-level test configuration classes from component scanning. Import the class explicitly into a test that needs it. That keeps a fixed test clock or another test-only bean from appearing unexpectedly in unrelated tests.
7.3. Does @TestConfiguration replace a production bean automatically?
No. It defines additional beans. The example uses a distinct bean name plus @Primary to select the fixed Clock for injection while retaining systemClock. If you need a different replacement mechanism, choose it deliberately and verify which bean the test receives.
8. Conclusion
Use a nested @TestConfiguration for a test-specific bean and an imported top-level class for reusable setup. Keep production configuration in the test context, and make bean selection explicit when both production and test beans share a type.