Testing · Lesson 77 of 95
@SpringBootTest
Learn @SpringBootTest in Spring Boot: start the full context, use MockMvc or a random port, override settings and replace beans, with a cinema example.
A car maker tests each part on a bench: the engine alone, the brakes alone. But before a car leaves the factory, someone also takes the whole car for a drive, with the engine, brakes, lights and steering all working together. Small tests tell you a part is good. The drive tells you the parts fit.
@SpringBootTest is that test drive. This guide on @SpringBootTest shows how to start the real Spring application inside a test, call it, and check that everything works together.
What is @SpringBootTest?
A normal unit test creates one class with new. A @SpringBootTest test asks Spring Boot to do what main does: find your @SpringBootApplication class, scan for beans, read application.properties and wire everything. Then you can inject any bean into the test with @Autowired.
It can also start a real web server on a random port, or a fake web layer, depending on how you configure it.
Why is it used?
- Integration testing. It checks that controllers, services and settings work together, not just alone.
- Catches wiring mistakes. A missing bean or a wrong setting shows up when the context starts.
- Tests real behaviour. Requests go through the same filters and converters as in production.
- Flexible. You can override settings or replace one bean with a mock.
Because it starts everything, it is slower than a unit test. Use it for a few important checks, and use small tests for the rest.
How it works
Before the test class runs, Spring Boot builds the application context once. Every test method that needs it receives beans from that context. If another test class asks for the same configuration, the built context is reused from a cache.
texttest class starts | v find @SpringBootApplication | v build the context (beans + settings) | v inject into the test | v run test methods
The cache is what keeps a big suite bearable. Two test classes with the same setup share one context. Change anything, such as a property or a mock bean, and Spring builds a new context for that class. In our run, the first test needed about ten seconds to build the context, and a class with a changed property built its own.
Web Environments
| Setting | What happens |
|---|---|
MOCK (default) | No real server. A mock web layer is used. |
RANDOM_PORT | A real server starts on a free port |
DEFINED_PORT | A real server starts on the configured port |
NONE | No web environment at all |
With the default mock environment, you call the app through MockMvc. With RANDOM_PORT you make real HTTP calls, and the @LocalServerPort annotation gives you the port that was chosen.
Real-Life Example
Regal Cinema has a small service that tells how many seats are left for a show. There are 100 seats in the settings file and 30 are already booked. A unit test can check the subtraction. But only a full test shows that the setting is really read from the file, that the controller is wired to the service, and that the web address works. That is the job of @SpringBootTest.
Code Example
Let's build the seat service and test it four ways: the whole context, a changed setting, MockMvc and a real server.
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.regal</groupId> <artifactId>shows</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiescinema.total-seats=100
File: ShowsApplication.java in package com.regal.shows
javapackage com.regal.shows; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ShowsApplication { public static void main(String[] args) { SpringApplication.run(ShowsApplication.class, args); } }
File: ShowService.java in package com.regal.shows
javapackage com.regal.shows; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class ShowService { private final AtomicInteger booked = new AtomicInteger(30); private final int totalSeats; public ShowService(@Value("${cinema.total-seats}") int totalSeats) { this.totalSeats = totalSeats; } public int seatsLeft() { return totalSeats - booked.get(); } }
File: ShowController.java in package com.regal.shows
javapackage com.regal.shows; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ShowController { private final ShowService service; public ShowController(ShowService service) { this.service = service; } @GetMapping("/seats") public String seats() { return "Seats left: " + service.seatsLeft(); } }
File: ShowContextTest.java in src/test/java/com/regal/shows
javapackage com.regal.shows; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ShowContextTest { @Autowired private ShowService service; @Test void contextLoadsAndReadsTheRealSettings() { assertThat(service.seatsLeft()).isEqualTo(70); } }
File: ShowPropertyTest.java in src/test/java/com/regal/shows
javapackage com.regal.shows; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest(properties = "cinema.total-seats=50") class ShowPropertyTest { @Autowired private ShowService service; @Test void testCanOverrideASetting() { assertThat(service.seatsLeft()).isEqualTo(20); } }
File: ShowMockMvcTest.java in src/test/java/com/regal/shows
javapackage com.regal.shows; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc class ShowMockMvcTest { @Autowired private MockMvc mvc; @Test void seatsEndpointAnswers() throws Exception { mvc.perform(get("/seats")) .andExpect(status().isOk()) .andExpect(content().string("Seats left: 70")); } }
File: ShowRandomPortTest.java in src/test/java/com/regal/shows
javapackage com.regal.shows; import static org.assertj.core.api.Assertions.assertThat; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ShowRandomPortTest { @LocalServerPort private int port; @Test void realServerAnswersOnARandomPort() throws Exception { HttpRequest request = HttpRequest.newBuilder( URI.create("http://localhost:" + port + "/seats")).build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).isEqualTo("Seats left: 70"); } }
Run the tests:
bash./mvnw test
Output:
textTests run: 4 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS
Maven prints the four counts on one line; they are split here for reading. All four test classes passed, each with one test.
Code Explained
@SpringBootTestalone findsShowsApplication, because the test sits in the same package or below it. The first test injectsShowServiceand proves that the setting 100 was read, with 30 booked leaving 70.properties = "cinema.total-seats=50"overrides one setting for this test class only. The answer becomes 20.@AutoConfigureMockMvcadds aMockMvcbean. We call/seatswithout any server and check the status and the body. In Spring Boot 4 this annotation lives in the packagewebmvc.test.autoconfigureunderorg.springframework.boot, and the web test starter (the webmvc one ending in-test) brings it.RANDOM_PORTstarts a real Tomcat on a free port.@LocalServerPortinjects that port. We call it with Java's ownHttpClient, so the test needs no extra library.- The test starter is added with
testscope, so it never reaches the production jar.
When to Choose It
Ask what could go wrong. If the risk is inside one method, write a unit test. If the risk is that two parts are joined wrongly, or that a setting is misread, or that a request is mapped to the wrong address, write a @SpringBootTest. A good suite has many small tests and a handful of these full ones, like a factory that checks every part on the bench but drives each model only once.
Overriding and Replacing Things
You have three easy tools. Use properties for a single setting. Use @ActiveProfiles("test") to load application-test.properties on top of the defaults. Use @MockitoBean to replace one bean with a mock, for example a payment gateway. Each of these makes the context different, so Spring builds a fresh one. Keep the number of different setups small if you want your suite fast.
Common Mistakes
- Test class in the wrong package. If the test cannot find the
@SpringBootApplicationclass, start-up fails. Keep tests in the same package tree as the main class. - Real database in tests. A full context may connect to a real database. Use an in-memory or container database for tests.
- Forgetting the web environment.
@LocalServerPortonly works withRANDOM_PORTorDEFINED_PORT. - Many different contexts. Every unique combination of properties, profiles and mock beans builds a new context.
- Old imports. In Spring Boot 4,
@AutoConfigureMockMvcmoved to a new package. Let your IDE fix imports from the new starter.
Interview Questions
What does `@SpringBootTest` do?
Ans:It starts the full Spring application context for a test so you can test beans and settings working together.
What is the difference between `MOCK` and `RANDOM_PORT`?
Ans:MOCK uses a fake web layer with no server. RANDOM_PORT starts a real server on a free port.
How do you replace one bean in a full test?
Ans:Declare a field with @MockitoBean in the test class.
Key Points to Remember
@SpringBootTestruns the real application context inside a test.- It is slower, so use it for integration checks, not for simple logic.
- The default web environment is a mock one, used with
MockMvc. RANDOM_PORTstarts a real server, and@LocalServerPortgives its port.properties,@ActiveProfilesand@MockitoBeanchange what the test sees.- Identical setups share a cached context.
Frequently Asked Questions
What is @SpringBootTest used for?
It is used for integration tests that need the whole application, such as checking that a controller, service and settings file work together.
Is @SpringBootTest slow?
It is slower than a unit test because it starts Spring. The context is cached, so classes with the same setup share it.
Do I need @ExtendWith with @SpringBootTest?
No. @SpringBootTest already registers the Spring extension for JUnit.
How is @SpringBootTest different from a slice test?
A slice test such as @WebMvcTest loads only one layer. @SpringBootTest loads everything.
Related Topics
- @WebMvcTest and MockMvc: load only the web layer for faster tests.
- @DataJpaTest: load only the database layer.
- Mockito in Spring Boot: replace beans with mocks.
- Spring Profiles: use a test profile with its own settings.
Practice Problems
Try each problem on your own first. Both projects use the web starter and the web test starter, so each brings its own pom.xml.
Easy: Bakery Context Test
SweetCrumbs keeps bakery.cake-price=450 in application.properties. Build PriceService with int priceFor(int cakes) that multiplies the settings value by the number of cakes. Write a @SpringBootTest that injects the service and checks that 3 cakes cost 1350.
Show answerHide answer
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.sweetcrumbs</groupId> <artifactId>bakery</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesbakery.cake-price=450
File: BakeryApplication.java in package com.sweetcrumbs
javapackage com.sweetcrumbs; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BakeryApplication { public static void main(String[] args) { SpringApplication.run(BakeryApplication.class, args); } }
File: PriceService.java in package com.sweetcrumbs
javapackage com.sweetcrumbs; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class PriceService { private final int cakePrice; public PriceService(@Value("${bakery.cake-price}") int cakePrice) { this.cakePrice = cakePrice; } public int priceFor(int cakes) { return cakePrice * cakes; } }
File: PriceServiceTest.java in src/test/java/com/sweetcrumbs
javapackage com.sweetcrumbs; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class PriceServiceTest { @Autowired private PriceService service; @Test void threeCakesCost1350() { assertThat(service.priceFor(3)).isEqualTo(1350); } }
Run ./mvnw test. Maven reports (the counts are split over lines here):
textTests run: 1 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS
Medium: Library API with a Mocked Catalog
Lakeview Library has a CatalogClient bean that would call a slow remote catalog. Build GET /books/count returning Books: N, where N comes from CatalogClient.countBooks(). Write a @SpringBootTest with MockMvc that replaces CatalogClient with @MockitoBean returning 250, and checks the status and the body.
Show answerHide answer
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.lakeview</groupId> <artifactId>library</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: LibraryApplication.java in package com.lakeview
javapackage com.lakeview; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class LibraryApplication { public static void main(String[] args) { SpringApplication.run(LibraryApplication.class, args); } }
File: CatalogClient.java in package com.lakeview
javapackage com.lakeview; import org.springframework.stereotype.Component; @Component public class CatalogClient { public int countBooks() { // A real client would call a remote catalog here. return 1; } }
File: BookController.java in package com.lakeview
javapackage com.lakeview; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BookController { private final CatalogClient client; public BookController(CatalogClient client) { this.client = client; } @GetMapping("/books/count") public String count() { return "Books: " + client.countBooks(); } }
File: BookControllerTest.java in src/test/java/com/lakeview
javapackage com.lakeview; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc class BookControllerTest { @Autowired private MockMvc mvc; @MockitoBean private CatalogClient client; @Test void countComesFromTheMock() throws Exception { when(client.countBooks()).thenReturn(250); mvc.perform(get("/books/count")) .andExpect(status().isOk()) .andExpect(content().string("Books: 250")); } }
Run ./mvnw test. Maven reports (the counts are split over lines here):
textTests run: 1 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS