Design a robust custom starter: how do you expose typed configuration and how do you test the auto-configuration in isolation?
answer
- @ConfigurationProperties + @EnableConfigurationProperties
- configuration-processor → IDE metadata json
- ApplicationContextRunner for isolated tests
- AutoConfigurations.of(...) not @Import
- FilteredClassLoader hides a class to test OnClass back-off
basics
~10 sExpose config with a @ConfigurationProperties class (bound via @EnableConfigurationProperties in the auto-config). Test with ApplicationContextRunner, which spins up a lightweight context, applies your auto-config and various properties/classpath conditions, and asserts which beans exist.
solid answer
~40 sFor typed config, define a @ConfigurationProperties(prefix = "acme.greeting") POJO and activate it from the auto-config with @EnableConfigurationProperties(GreetingProperties.class), so users configure via acme.greeting.* keys with relaxed binding and validation. Add spring-boot-configuration-processor to generate IDE metadata. Beans read the properties for defaults, still guarded by @ConditionalOnMissingBean so apps can override. For testing, use ApplicationContextRunner: it builds a real-but-minimal context without starting a full app, lets you register your auto-config via withConfiguration(AutoConfigurations.of(...)), set properties with withPropertyValues, and simulate a missing classpath dependency with FilteredClassLoader. You then assert context.hasBean(...) / getBean(...) or that the context backed off. This exercises every conditional path — property on/off, class present/absent, user-bean-overrides — fast and deterministically, which is exactly how Spring Boot tests its own auto-configs.
code
java · 36 lines@ConfigurationProperties(prefix = "acme.greeting")
public class GreetingProperties {
private String greeting = "Hello";
public String getGreeting() { return greeting; }
public void setGreeting(String g) { this.greeting = g; }
}
@AutoConfiguration
@EnableConfigurationProperties(GreetingProperties.class)
public class GreetingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
GreetingService greetingService(GreetingProperties props) {
return new GreetingService(props.getGreeting());
}
}
// --- test ---
class GreetingAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GreetingAutoConfiguration.class));
@Test void appliesDefault() {
runner.run(ctx -> assertThat(ctx).hasSingleBean(GreetingService.class));
}
@Test void bindsProperty() {
runner.withPropertyValues("acme.greeting.greeting=Hi")
.run(ctx -> assertThat(ctx.getBean(GreetingService.class).message()).isEqualTo("Hi"));
}
@Test void userBeanWins() {
runner.withUserConfiguration(CustomGreetingConfig.class)
.run(ctx -> assertThat(ctx).hasSingleBean(GreetingService.class)
.getBean(GreetingService.class)
.isSameAs(ctx.getBean("myGreeting")));
}
}go deeper
Knows @ConfigurationProperties exists for typed config; testing depth not expected.
Can wire @EnableConfigurationProperties and write a basic ApplicationContextRunner test.
Covers relaxed binding, configuration-processor metadata, and the main runner features (properties, user config).
Designs the full conditional test matrix (default/disabled/overridden/dependency-absent) with FilteredClassLoader and AutoConfigurations.of, and justifies the runner over @SpringBootTest.
**Goal.** A production-grade starter should be *configurable*, *overridable*, and *thoroughly tested across its conditional branches*. Two pillars: `@ConfigurationProperties` for config, `ApplicationContextRunner` for tests. **Typed configuration with @ConfigurationProperties.** - Define a POJO annotated `@ConfigurationProperties(prefix = "acme.greeting")` with fields like `greeting`, `enabled`, etc. Spring binds `acme.greeting.greeting=Hi` from any property source using *relaxed binding* (kebab-case, camelCase, env vars all map). You can add JSR-303 `@Validated` + constraints for fail-fast validation. - Activate binding from the auto-config with `@EnableConfigurationProperties(GreetingProperties.class)`. That registers the properties bean without needing `@Component`/scanning (which auto-configs avoid). Alternatively use constructor binding with `@ConfigurationPropertiesScan` in an app, but in a starter `@EnableConfigurationProperties` is the idiom. - Add the **`spring-boot-configuration-processor`** annotation processor to the autoconfigure module. It generates `META-INF/spring-configuration-metadata.json`, giving consumers IDE autocomplete and docs for your `acme.greeting.*` keys. Provide additional metadata (descriptions, defaults) via `META-INF/additional-spring-configuration-metadata.json` if desired. - Beans consume the properties for their defaults but stay guarded by `@ConditionalOnMissingBean` and optionally `@ConditionalOnProperty(prefix="acme.greeting", name="enabled", matchIfMissing=true)` so the whole feature is toggleable. **Testing with ApplicationContextRunner.** This is the canonical way Spring Boot tests auto-config, and interviewers love it because it proves you understand conditions. - `new ApplicationContextRunner()` creates a runner for a non-web context (`WebApplicationContextRunner` / `ReactiveWebApplicationContextRunner` for web). - `.withConfiguration(AutoConfigurations.of(GreetingAutoConfiguration.class))` registers your auto-config the *same way Spring Boot would* (respecting conditions and ordering), not as plain `@Import`. - `.withPropertyValues("acme.greeting.enabled=false")` injects properties to drive `@ConditionalOnProperty` branches. - `.withUserConfiguration(CustomConfig.class)` adds a user bean to verify `@ConditionalOnMissingBean` back-off. - `.withClassLoader(new FilteredClassLoader(GreetingService.class))` *hides* a class to test `@ConditionalOnClass` back-off — simulating the dependency being absent. - `.run(context -> { ... })` gives an `AssertableApplicationContext`; assert with AssertJ: `assertThat(context).hasSingleBean(GreetingService.class)`, `.doesNotHaveBean(...)`, `.getBean(...).satisfies(...)`, or `.getFailure()` for expected startup failures. - It's *fast* (no full app start, no web server) and isolated, so you can enumerate every conditional permutation. **Why not @SpringBootTest?** `@SpringBootTest` boots a whole application context and is heavyweight; it can't easily toggle classpath presence or assert back-off across many permutations. `ApplicationContextRunner` is purpose-built for auto-config unit testing. **Gotchas.** - Forgetting `@EnableConfigurationProperties` means the properties bean never exists and binding silently doesn't happen. - Using `@Import(GreetingAutoConfiguration.class)` in tests instead of `AutoConfigurations.of(...)` bypasses the ordering/filtering the real runtime applies — false confidence. - `FilteredClassLoader` must hide the *exact* class the condition references. - Constructor-bound `@ConfigurationProperties` needs the record/class registered; pair with `@EnableConfigurationProperties`, don't rely on component scanning inside a starter. - Add `spring-boot-autoconfigure-processor` too, so condition metadata is generated for faster startup filtering. **When to use.** Every published/shared starter should ship `@ConfigurationProperties` for anything a consumer might tune, and a suite of `ApplicationContextRunner` tests covering: default on, disabled via property, overridden by user bean, and dependency-absent back-off.
- Why use AutoConfigurations.of(...) in tests instead of @Import on the auto-config class?AutoConfigurations.of registers the class through the same auto-configuration machinery Spring Boot uses at runtime — honoring conditions, ordering, and metadata filtering. Plain @Import treats it as ordinary config and can mask ordering/condition bugs, giving false confidence.
- How would you test that your bean backs off when an optional dependency is missing from the classpath?Use .withClassLoader(new FilteredClassLoader(TheOptionalType.class)) on the ApplicationContextRunner. It hides that class so @ConditionalOnClass fails, and you assert the context doesNotHaveBean(...), proving graceful back-off.
- How do consumers get IDE autocomplete for your custom acme.greeting.* properties?Include the spring-boot-configuration-processor annotation processor in the autoconfigure module. It generates META-INF/spring-configuration-metadata.json from your @ConfigurationProperties, which IDEs read for completion and docs; you can enrich it via additional-spring-configuration-metadata.json.
saying these in an interview costs you the question
- Using @SpringBootTest to unit-test auto-config permutations
- Registering the auto-config with @Import instead of AutoConfigurations.of
- Forgetting @EnableConfigurationProperties so binding never happens
- Relying on component scanning to pick up @ConfigurationProperties in a starter
- Claiming FilteredClassLoader adds a class rather than hiding one