Here is the English translation of the article body:
Every time you write a test in Spring Boot, do you find yourself searching “should this be @SpringBootTest, or should I use @WebMvcTest?” all over again? You’ve probably also seen projects where everything was written with @SpringBootTest for convenience, and now the test suite takes several minutes to run.
This article provides a quick-reference table and a decision flow so you can instantly pick the right annotation based on the layer under test. Minimal samples for each annotation are included, but treat this as a hub article that points you to dedicated articles for detailed implementations.
The Quick-Reference Table (TL;DR)
Organized so you can look things up by what you’re testing, it comes down to this:
| Annotation | What gets started | Speed | Main use case | Mocking approach to pair with |
|---|---|---|---|---|
| None (plain JUnit) | Nothing | Fastest | Unit testing Services | Mockito |
@WebMvcTest | MVC-related Beans only | Fast | Verifying Controller input/output | @MockitoBean + MockMvc |
@DataJpaTest | JPA-related Beans only | Fast | Verifying Repository queries | No mocks, as a rule |
@JsonTest | Jackson-related only | Among the fastest | Verifying JSON serialization | Not needed |
@RestClientTest | Client Beans only | Fast | Verifying external API calls | MockRestServiceServer |
@SpringBootTest | All Beans | Slow | Integration testing across layers | Testcontainers / WireMock |
Other slice tests follow the same philosophy: @DataMongoTest for MongoDB, @JdbcTest for JDBC, and so on.
The decision flow is simple:
- If you want to verify Service logic, no annotation is needed. Write it with plain JUnit + Mockito
- Pick the slice test that matches the layer:
@WebMvcTestfor Controllers,@DataJpaTestfor Repositories, and so on - Use
@SpringBootTestonly for verification that cuts across multiple layers
The basic policy: “slice tests first, @SpringBootTest only for integration tests.”
Why “Everything with @SpringBootTest” Is Slow
@SpringBootTest starts an ApplicationContext containing all of your application’s Beans — including database connections and security configuration. Slice tests, on the other hand, only start the Beans for the target layer, so the per-startup cost is dramatically different.
You might be thinking, “But isn’t the context cached?” It’s true that Spring’s test framework reuses contexts with identical configurations. However, the cache only kicks in when configurations match exactly. Any per-test-class difference — a different @MockitoBean target, a property changed via @TestPropertySource — causes a cache miss, and a full startup runs each time.
In other words, the more you scatter @SpringBootTest around, the more your build time grows by “full startup × number of cache misses.” Move single-layer verification into slice tests and shrink that multiplication itself.
Controller Layer: @WebMvcTest + MockMvc
@WebMvcTest only starts MVC-related Beans such as Controllers, Filters, and Jackson configuration. Services and Repositories are not included, so replace the Services you depend on with @MockitoBean.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockitoBean
UserService userService;
@Test
void getUser_returnsOk() throws Exception {
when(userService.findById(1L))
.thenReturn(new UserResponse(1L, "alice"));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("alice"));
}
}
Patterns for building requests and verifying validation are covered in detail in the article on Controller testing with MockMvc.
Service Layer: No Annotation Needed
This is often overlooked, but unit testing a Service requires no Spring annotations at all. Without starting any context, you can do everything with just @ExtendWith(MockitoExtension.class) and @Mock / @InjectMocks.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
UserRepository userRepository;
@InjectMocks
UserService userService;
@Test
void findById_returnsUser() {
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "alice")));
UserResponse result = userService.findById(1L);
assertThat(result.name()).isEqualTo("alice");
verify(userRepository).findById(1L);
}
}
With no context startup, execution is nearly instant. Ideally, this is where you concentrate your business logic verification. For the basics of when / verify syntax, see the introductory article on testing with JUnit and Mockito.
Repository Layer: @DataJpaTest
@DataJpaTest starts only JPA-related Beans such as Repositories and the EntityManager, and by default automatically swaps the database for an in-memory one. Each test is automatically rolled back afterward, so there’s no data cleanup to worry about.
@DataJpaTest
class UserRepositoryTest {
@Autowired
TestEntityManager em;
@Autowired
UserRepository userRepository;
@Test
void findByEmail_returnsUser() {
em.persist(new User("alice", "[email protected]"));
em.flush();
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
}
}
If you want to verify against a production-equivalent database — for example, PostgreSQL-specific SQL — there’s an advanced approach that combines this with Testcontainers. For details on when to use which, see the article on slice testing with @DataJpaTest and the Testcontainers article.
JSON Verification: @JsonTest
If all you want to verify is Jackson configuration — date formats, property naming conventions, and the like — there’s no need to start a Controller. @JsonTest is the lightest-weight slice test, starting only Jackson-related Beans.
@JsonTest
class UserResponseJsonTest {
@Autowired
JacksonTester<UserResponse> json;
@Test
void serialize_formatsDate() throws Exception {
var response = new UserResponse("alice", LocalDate.of(2026, 9, 2));
assertThat(json.write(response))
.extractingJsonPathStringValue("$.created_at")
.isEqualTo("2026-09-02");
}
}
This comes in handy on projects where ObjectMapper customizations have started to pile up.
External API Clients: @RestClientTest
For clients that call external APIs with RestClient or RestTemplate, there’s @RestClientTest. It starts only the target client Bean and a MockRestServiceServer, letting you verify response handling without any real HTTP communication.
@RestClientTest(WeatherClient.class)
class WeatherClientTest {
@Autowired
WeatherClient weatherClient;
@Autowired
MockRestServiceServer server;
@Test
void getWeather_parsesResponse() {
server.expect(requestTo("/weather/tokyo"))
.andRespond(withSuccess("{\"condition\":\"sunny\"}",
MediaType.APPLICATION_JSON));
assertThat(weatherClient.getWeather("tokyo").condition())
.isEqualTo("sunny");
}
}
If you’re using WebClient, or want to verify behavior including timeouts and retries, WireMock — which spins up a real HTTP server — is a better fit. We compare the two in the article on external API testing with WireMock.
When You Should Use @SpringBootTest
After reading this far, @SpringBootTest might look like the villain, but it’s not something to avoid — it’s something to use in the right place. Scenario verification that cuts through Controller → Service → Repository, and verification of security configuration, Bean definitions, and property resolution — that’s where @SpringBootTest shines.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiIntegrationTest {
@Autowired
TestRestTemplate restTemplate;
@Test
void getUser_endToEnd() {
var response = restTemplate.getForEntity("/api/users/1", UserResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
For webEnvironment, choose between the default MOCK (mock servlet environment + MockMvc) and RANDOM_PORT, which actually opens a port (+ TestRestTemplate). How to structure integration tests that include a real database or Kafka is explained in the article on integration testing with @SpringBootTest and Testcontainers. Keep the number of integration tests small and push everyday verification into slice tests.
@MockBean Is Deprecated — Move to @MockitoBean
One thing to watch out for: the long-standing @MockBean / @SpyBean were deprecated in Spring Framework 6.2 (Spring Boot 3.4) and replaced by Spring Framework’s own @MockitoBean / @MockitoSpyBean. All samples in this article consistently use @MockitoBean.
Changing the import from org.springframework.boot.test.mock.mockito.MockBean to org.springframework.test.context.bean.override.mockito.MockitoBean and replacing the annotation name gets you most of the way there, but there are subtle incompatibilities. For details, see the migration article from @MockBean to @MockitoBean.
Tool Compatibility Table
Finally, here’s a summary of which mocking and testing tools pair with which annotations. An easy way to remember it: “the annotation determines the scope of the context, and the tool fakes what’s outside the boundary.”
| Test target | Annotation | Tools to combine |
|---|---|---|
| Service | None | Mockito (when / verify) |
| Controller | @WebMvcTest | MockMvc + @MockitoBean |
| Repository | @DataJpaTest | Testcontainers if needed |
| JSON conversion | @JsonTest | JacksonTester |
| External API client | @RestClientTest | MockRestServiceServer |
| Integration test | @SpringBootTest | Testcontainers / WireMock |
Summary
When in doubt, think in this order:
- Identify the layer under test
- Pick the matching slice test — or plain JUnit + Mockito for Services
- Use
@SpringBootTestonly for verification that spans layers
“Slice tests first, @SpringBootTest only for integration tests.” This policy alone can dramatically change your test execution time. Once you’ve decided which layer to dig deeper into, head over to the dedicated articles introduced in each section.