The labs on this site need JavaScript to run. The same content is written out below.
Learn › Spring › Other features
When tests run, what goes into the Spring context, and how many times is it created?
There are several ways to test a single controller. You can create the objects with new and call them without Spring, load all the beans with @SpringBootTest, or load only the web layer with @WebMvcTest. Depending on the approach, different beans go into the context (the container that creates and holds the beans), and unless you fill in the missing beans with mocks, the test cannot even start. Run code in which OrderController → OrderService → OrderRepository depend on each other in that order with two test classes, and check the result of each test and the number of times a context was created.
Checked against real execution · 36 combinations were run, each in a new JVM, on Spring Boot 4.1.1, spring-test 7.0.9, JUnit Jupiter 6.0.3, and Mockito 5.23.0 and checked against the results
// First test class
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
MockMvc mockMvc; // a tool that sends requests without a server
@Test
void returnsOrderName() throws Exception {
mockMvc.perform(get("/orders/1"))
.andExpect(status().isOk())
.andExpect(content().string("keyboard"));
}
}
// Second test class, which runs next
@WebMvcTest(OrderController.class)
class OrderControllerSecondTest {
// The fields and returnsOrderName() are the same as in OrderControllerTest
}
// ---- Code under test: controller → service → repository ----
@RestController
class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/orders/{id}")
public String name(@PathVariable Long id) {
return orderService.findName(id);
}
}
@Service
class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public String findName(Long id) {
return orderRepository.findName(id);
}
}
@Repository
class OrderRepository {
// A map used in place of a database
private final Map<Long, String> db = Map.of(1L, "keyboard");
public String findName(Long id) {
return db.get(id);
}
}
All 36 combinations were actually run on this PC and checked against the results (3 testing approaches × 4 mock choices × 4 second-class choices; for a test without Spring, the second-class setting has no meaning, so only 4 are counted). The versions are Spring Boot 4.1.1, Spring Framework and spring-test 7.0.9, JUnit Jupiter 6.0.3, Mockito 5.23.0, and JDK 21. The two test classes were run one after the other with the JUnit Platform Launcher, without Gradle or Maven, and because the context cache lives inside the JVM, a new Java process was started for each combination. What was compared: the results of the two classes (passed, assertion failed, context failed to load, failure that was not retried), whether the controller, service, and repository that each class used were real, Mockito mocks, or absent, the number of times the creation of a context was started (counted as the number of times the constructor of the configuration class was called, including the ones that failed to load), the number of times loading succeeded, and whether the two classes received the same context object. Spring Boot 4 has no @MockBean (it was confirmed that the spring-boot-test jar of 4.1.1 does not contain that class) and uses @MockitoBean of Spring Framework (org.springframework.test.context.bean.override.mockito). @WebMvcTest and @AutoConfigureMockMvc are in the org.springframework.boot.webmvc.test.autoconfigure package of the spring-boot-webmvc-test module. The package names differ from those of 3.x, so take care when you copy import statements. How auto-configuration was controlled: the classpath of this PC also contains Spring Security, H2 and HikariCP, Hibernate, Spring Data JPA, and Actuator. In the check runs, the exclude attribute of @SpringBootApplication left out five Spring Security auto-configurations and the DataSource, Hibernate JPA, and Spring Data JPA repositories auto-configurations. When the lab was run without excluding them, the request of @SpringBootTest was blocked by Spring Security and became 401, and @WebMvcTest passed as before. (This is the result of a separate run made while the lab was being built, and it is not in the saved check output. On this classpath, the list of auto-configurations that @WebMvcTest loads did not include Spring Security. The behavior when the spring-boot-security-test module is present was not verified.) The @SpringBootApplication class is omitted on the screen. Differences between the code on the screen and the code used for the check: in the check code, the test classes have different names, they inherit the same body from an abstract class, and there are additional lines that record whether an object is real or a mock and additional code that counts the contexts. "Has one more @MockitoBean" is the case that replaces one more thing that the first class did not replace (OrderRepository, or OrderService if that is already replaced). The database is replaced with an in-memory map both on the screen and in the runs. The threshold after which a context that has failed once with the same configuration is not created again is spring.test.context.failure.threshold, and its default is 1 (confirmed from the constant in spring-test 7.0.9). For @DirtiesContext, only the default behavior when it is put on a class (the context is closed after the tests of the class finish) is covered. On this PC, the number of bean definitions in the context was 303 for @SpringBootTest and 130 for @WebMvcTest (131 when a mock bean is added), and these numbers change with the classpath and the exclude above. The time taken differs on every run, so it is not in the result table. In one run on this PC, the first class took about 0.7s to 0.9s for the test without Spring (including Mockito setup), about 2.7s for @WebMvcTest, and about 3.4s to 4.4s for @SpringBootTest, and the second class, which reused the context, took 0.05s or less. On JDK 21, Mockito attaches itself as an agent at run time in order to create mocks and prints a warning, but this does not affect the behavior, so it was left as it is. The @Transactional rollback of test methods, other slices such as @DataJpaTest, @MockitoSpyBean, the maximum size of the cache, and the case where a build tool splits the tests across several processes (the cache is separate for each process) are not covered and were not verified by execution either.
Previous lab: Actuator and health checks · Next lab: Calling external APIs and timeouts
Struck-through text was removed from the first version of the code; highlighted text was added.