Building a custom Spring Boot starter (autoconfigure + starter modules, AutoConfiguration.imports, conditions, @ConfigurationProperties), wrapping a third-party library as a starter, common starters, alternatives to field @Autowired, @Mock vs @MockBean/@MockitoBean, JUnit 4 vs 5, lifecycle annotations in practice, controller slice tests with MockMvc, integration tests against external APIs (WireMock/MockRestServiceServer/Testcontainers), and DevTools in development vs production.
Published September 25, 2026
These questions check whether you build platform pieces (starters) and test properly. Use current APIs:
AutoConfiguration.imports, not spring.factories.@MockBean in favour of @MockitoBean.@BeforeEach and @AfterEach, not @Before and @After.Short answer: A starter packages dependencies + auto-configuration + defaults, so any team adds one dependency and gets a correctly configured feature. The convention is two modules:
acme-audit-spring-boot-autoconfigure: the @AutoConfiguration classes, @ConfigurationProperties, and conditions.acme-audit-spring-boot-starter: an (almost) empty module that depends on the autoconfigure module and on the library itself.Register the auto-configuration in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
@AutoConfiguration
@ConditionalOnClass(AuditClient.class) // only when the library is present
@EnableConfigurationProperties(AuditProperties.class)
@ConditionalOnProperty(prefix = "acme.audit", name = "enabled", havingValue = "true", matchIfMissing = true)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean // the application can override it
AuditClient auditClient(AuditProperties props) {
return AuditClient.builder().endpoint(props.endpoint()).timeout(props.timeout()).build();
}
}
@ConfigurationProperties("acme.audit")
public record AuditProperties(URI endpoint, @DefaultValue("2s") Duration timeout, @DefaultValue("true") boolean enabled) { }
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.acme.audit.autoconfigure.AuditAutoConfiguration
Key points to cover:
@ConditionalOnMissingBean.spring-boot-configuration-processor, so IDEs autocomplete your properties.ApplicationContextRunner.Common trap: the source says to register in spring.factories. That was removed for auto-configurations in Boot 3. Use AutoConfiguration.imports.
Learn it in depth → Auto-Configuration Mechanism
Short answer:
optional in the autoconfigure module).@ConfigurationProperties with safe defaults.@AutoConfiguration, guarded by:
@ConditionalOnClass (the library is on the classpath);@ConditionalOnProperty (a feature toggle);@ConditionalOnMissingBean (the user can override it).@ConditionalOnEnabledHealthIndicator), tracing, and customizer interfaces (AuditClientCustomizer) for tweaks.@AutoConfiguration(after = …) if it depends on other auto-configurations.ApplicationContextRunner, then publish to your artifact repository, with a BOM for versions.new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AuditAutoConfiguration.class))
.withPropertyValues("acme.audit.endpoint=http://localhost:9999")
.run(ctx -> assertThat(ctx).hasSingleBean(AuditClient.class));
Short answer:
spring-boot-starter-web (MVC + Tomcat), -webflux (Reactor + Netty).-data-jpa, -jdbc, -data-mongodb, -data-redis.-security, -oauth2-resource-server, -oauth2-client.-validation, -actuator.-amqp, spring-kafka (not a starter, but auto-configured).-cache, -thymeleaf, -mail.-test (JUnit 5, Mockito, AssertJ, MockMvc, JSONassert).spring-boot-docker-compose and -testcontainers for local development and tests.Starters are curated dependency sets, with versions managed by Boot's BOM. The auto-configuration they trigger lives in spring-boot-autoconfigure.
@Autowired?Short answer:
final, the object is fully initialised, it's easy to test with new, and circular dependencies are exposed.@Inject (JSR-330), and @Resource (by name).ObjectProvider<T>: lazy or optional lookup, or several candidates.@Bean method parameters in configuration classes.@Service
@RequiredArgsConstructor // Lombok generates the constructor; no @Autowired anywhere
class OrderService {
private final OrderRepository orders;
private final ObjectProvider<FraudChecker> fraudChecker; // optional
}
Common trap: "constructor injection doesn't rely on Spring annotations" is only true if you don't annotate. The real reasons to prefer it are immutability, completeness and testability. Field injection hides dependencies, and needs reflection in tests.
Learn it in depth → Dependency Injection
@Mock and @MockBean (@MockitoBean)?Short answer:
@Mock (Mockito): creates a mock in a plain unit test, with no Spring context. It's injected with @InjectMocks, or manually. It's fast.@MockBean: replaces or adds a bean inside the Spring ApplicationContext for slice or integration tests (@WebMvcTest, @SpringBootTest). It's deprecated in Boot 3.4, in favour of Spring Framework's @MockitoBean (and @MockitoSpyBean).Key points to cover:
@Mock, and standardise the mocks in slice tests.Short answer:
@RunWith and @Rule. JUnit 5 has one extension model (@ExtendWith), and several extensions can compose.@ParameterizedTest with rich sources;@Nested, @DisplayName, @Tag;assertThrows, assertAll, assertTimeout;@Before/@After/@BeforeClass/@AfterClass became @BeforeEach/@AfterEach/@BeforeAll/@AfterAll. @Ignore became @Disabled.Choose JUnit 5 (or JUnit 6, which requires Java 17+) for anything new. Boot 2.2+ defaults to it. Keep JUnit 4 only for legacy suites, run through Vintage while migrating.
Short answer:
@BeforeAll: expensive, shared, read-only setup, once per class. It's static unless you use @TestInstance(PER_CLASS). For example, start a Testcontainers database, or load reference data.@BeforeEach: fresh state per test: new objects, reset mocks, insert test data. That keeps tests independent and order-free.@AfterEach: clean up per-test side effects: delete rows (or roll back with @Transactional tests), delete temporary files, clear caches and the MDC.@AfterAll: release the shared resources.@Testcontainers
@SpringBootTest
class OrderRepositoryIT {
@Container @ServiceConnection
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16"); // started once (like @BeforeAll)
@Autowired OrderRepository orders;
@BeforeEach void seed() { orders.save(Order.pending(customerId, items)); }
@AfterEach void clean() { orders.deleteAll(); }
@Test void findsPendingOrders() { assertThat(orders.findByStatus(PENDING)).hasSize(1); }
}
Common trap: the source uses the JUnit 4 names @Before and @After next to JUnit 5's @BeforeAll. In JUnit 5, use @BeforeEach and @AfterEach. Mixing the two APIs means the JUnit 4 annotations are silently ignored by Jupiter.
Short answer: Use @WebMvcTest(ProductController.class). It loads only the MVC slice: the controller, the advice, converters, filters and security. Then inject MockMvc (or MockMvcTester in Boot 3.4+), and mock the service with @MockitoBean. Assert the status, JSON body, headers and validation errors, without starting a server.
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired MockMvc mvc;
@MockitoBean ProductService service;
@Test
void createReturns201WithLocation() throws Exception {
given(service.create(any())).willReturn(new ProductDto(ID, "Pen", new BigDecimal("20")));
mvc.perform(post("/products").contentType(APPLICATION_JSON).content("""
{"name":"Pen","price":20,"stock":5,"sku":"PEN-0001"}"""))
.andExpect(status().isCreated())
.andExpect(header().string("Location", endsWith("/products/" + ID)))
.andExpect(jsonPath("$.name").value("Pen"));
}
@Test
void invalidBodyReturns400() throws Exception {
mvc.perform(post("/products").contentType(APPLICATION_JSON).content("{\"name\":\"\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[*].field", hasItem("name")));
then(service).shouldHaveNoInteractions();
}
}
Key points to cover:
@WithMockUser or .with(jwt()), and .with(csrf()) for form posts.Short answer: Test your real HTTP client code against a fake server, rather than mocking the client bean away (which the source suggests, and which skips serialisation, headers, timeouts and error mapping):
@DynamicPropertySource, and stub the responses, including error and latency cases: 500s, timeouts, malformed bodies. That verifies retries, circuit breakers and fallbacks.MockRestServiceServer, for RestTemplate/RestClient bound through @RestClientTest: a lighter, in-JVM version.@SpringBootTest
@WireMockTest(httpPort = 9561)
class ShippingClientIT {
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) { r.add("shipping.base-url", () -> "http://localhost:9561"); }
@Autowired ShippingClient client;
@Test
void mapsCarrierTimeoutToUnavailable() {
stubFor(get(urlPathEqualTo("/rates")).willReturn(aResponse().withFixedDelay(3_000)));
assertThatThrownBy(() -> client.rates(PARCEL)).isInstanceOf(ShippingUnavailableException.class);
}
}
@MockitoBean on the client is fine for tests whose focus is elsewhere, such as service logic.
Short answer: Don't ship it.
java -jar), or when it detects a production-like launch.developmentOnly (Gradle) or <optional>true</optional> (Maven), so it's excluded from the repackaged artifact.spring.devtools.remote.secret) is a remote code-update endpoint, and a serious security hole if exposed.spring-boot-devtools jar.Short answer:
~/.config/spring-boot/.spring-boot-docker-compose starts your dependencies on application start.Key points to cover:
TestApplication with @ServiceConnection), and with hot-swap agents (JRebel, or the JetBrains Runtime's enhanced HotSwap), where available.Q: What's the difference between @SpringBootTest, @WebMvcTest and @DataJpaTest?
A: @SpringBootTest loads the full context (optionally with a real server: webEnvironment = RANDOM_PORT). Slices load only part of it:
@WebMvcTest: the MVC layer;@DataJpaTest: JPA repositories, an embedded or Testcontainers database, and transactional rollback per test;@JsonTest, @RestClientTest, @DataMongoTest.Slices are faster, and more focused.
Q: How do you keep a Spring test suite fast?
A: Maximise context caching: consistent configuration, few distinct @MockitoBean sets, and no @DirtiesContext unless necessary. Favour plain unit tests, reuse Testcontainers through singleton containers, and run the tests in parallel where they're isolated.
Q: How do you override an auto-configured bean from a starter?
A: Define your own bean of the same type. @ConditionalOnMissingBean makes the starter back off. Or set properties or use the customizer beans the starter exposes. To disable it entirely, use spring.autoconfigure.exclude.
Q: What does @ServiceConnection do?
A: Available since Boot 3.1, it lets a Testcontainers container automatically provide the connection details (URL, username, password) to the matching auto-configuration. It replaces most @DynamicPropertySource boilerplate.