Skip to main content

Spring Boot 4 Test Slices: @WebMvcTest, @JsonTest, @RestClientTest and @DataJpaTest Compared

Spring Boot 4.1.2 test slices run side by side on one app: what @WebMvcTest, @JsonTest, @RestClientTest and @DataJpaTest each build (35 to 251 bean definitions), the exact errors a missing mock, an unnamed client and @EnableJpaAuditing produce, and the Boot 4 package moves that break every old import. Every figure comes from a committed test run.

You have a controller that returns a book as JSON, and you want to check one thing: a missing id comes back as a 404 with a readable body. Starting the whole application for that — the database, the HTTP client, every bean you ever wrote — is like starting a car to test the horn. A test slice is Spring Boot’s answer. It is a test annotation that builds only the part of the application one kind of test needs. This post builds one small book-catalogue app and tests it four ways — @WebMvcTest, @JsonTest, @RestClientTest and @DataJpaTest — then runs the same app under a full @SpringBootTest so you can see exactly what each slice leaves out. By the end you will know which beans each slice creates (35 to 251 on this app), what the error looks like when a slice is missing something, and the Spring Boot 4 change that stops the imports of almost every old slice test from compiling. Every number and message below comes from a file in the companion module, written by the tests themselves.
Versions tested. Spring Boot 4.1.2 (published 22 October 2026; the latest GA — 4.2.0 has only release candidates so far, see the 4.2 checklist), Spring Framework 7.0.10, Hibernate ORM 7.4.13.Final, Jackson 3 (tools.jackson), H2 in memory, JDK Temurin 25.0.5 LTS. Everything is regenerated by ./scripts/run-all.sh. Startup times are indicative: they come from one cold JVM per test class on a shared sandbox and will differ on your machine.

A test slice is a smaller Spring context, not a smaller test

Spring keeps your objects — controllers, services, repositories, clients — in a container called the application context, and every object in it is a bean. A normal @SpringBootTest builds all of them before your first assertion runs. A slice annotation builds a deliberately smaller set, chosen for one layer. The test itself is the same JUnit test you already know; what changes is how much Spring has to build around it. The app under test is a small book catalogue: a BookController and an ApiExceptionHandler (the web layer), a BookService that calls a BookRepository (the service layer), a Spring Data BookRepository with a JPA Book (the data layer), an IsbnLookupClient that calls an external ISBN service over HTTP, and a PriceJson that writes a book’s price as a string like "INR 499.00". The last class, AuditLogger, is a plain @Component that nothing in the web layer needs. It is the canary: if it shows up in a context, that context built more than the slice promised.
Bean definitions in the context each test builds (same application) @JsonTest 35 @RestClientTest 53 @DataJpaTest 80 @WebMvcTest 126 @SpringBootTest 251 Every slice builds a fraction of the full context. Source: the last row of output/20-slice-matrix.txt.
The chart is the whole argument of this post. The same application, started five ways, ends up with between 35 and 251 bean definitions. Read each bar as “how much Spring must build before the test can start”, and read the choice of annotation as “which part of the app am I actually testing?”. The numbers are the last row of 20-slice-matrix.txt.
Going deeper: how a slice decides what to load Two things set a slice’s contents. First, it still starts from your @SpringBootApplication class, but the component scan lets through only some kinds of component. In this module’s run the web slice kept BookController, ApiExceptionHandler and the @JacksonComponent, and dropped the plain @Component (see the grid in the comparison section below). Second, it switches on a short list of auto-configurations instead of all of them. The Boot reference lists these per slice — for example @DataJpaTest imports DataJpaRepositoriesAutoConfiguration and HibernateJpaAutoConfiguration — but its table does not include @JsonTest, so for that slice the run itself is the evidence: JsonMapper and the @JacksonComponent, nothing else. One consequence is easy to miss: because every slice starts from your @SpringBootApplication class, anything you annotate that class with applies to every slice. The @EnableJpaAuditing trap in the @DataJpaTest section shows what that costs.

@WebMvcTest checks HTTP behaviour without a database

@WebMvcTest(BookController.class) builds Spring MVC, the controller you name, any @ControllerAdvice, Jackson, and a MockMvc object that calls the controller in the same JVM — no port, no Tomcat. What it does not build is anything the controller depends on. BookController needs a BookService, so the test has to supply one, and @MockitoBean puts a Mockito mock of that type into the context. The class header and its three fields are in WebMvcSliceTest.java:
@WebMvcTest(BookController.class)
class WebMvcSliceTest {

    @Autowired
    MockMvc mvc;

    @Autowired
    ApplicationContext ctx;

    @MockitoBean
    BookService service;
The test then stubs the mock and makes two requests — one for a book that exists, one for an id the service reports as missing:
        given(service.get(1L)).willReturn(
                new Book("9780134685991", "Effective Java", "Joshua Bloch", new BigDecimal("499"), LocalDate.of(2018, 1, 6)));
        given(service.get(99L)).willThrow(new BookNotFoundException(99L));

        String ok = mvc.perform(get("/api/books/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title").value("Effective Java"))
                .andExpect(jsonPath("$.price").value("INR 499.00"))
                .andExpect(jsonPath("$.published_on").value("2018-01-06"))
                .andReturn().getResponse().getContentAsString();

        String missing = mvc.perform(get("/api/books/99"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.title").value("Book not found"))
                .andReturn().getResponse().getContentAsString();
What came back, from 01-webmvc-slice.txt:
GET /api/books/1   -> 200 {"isbn":"9780134685991","title":"Effective Java","author":"Joshua Bloch","price":"INR 499.00","published_on":"2018-01-06"}
GET /api/books/99  -> 404 {"detail":"No book with id 99","instance":"/api/books/99","status":404,"title":"Book not found"}
Look at the price. The controller returns a Price, and the string INR 499.00 comes from PriceJson, a @JacksonComponent. The web slice loaded it, which is why the JSON has the right shape. The 404 body, with its title, comes from ApiExceptionHandler — a @RestControllerAdvice that the slice also loaded. So the error mapping is testable with no service layer at all.
What @WebMvcTest(BookController.class) buildsMockMvcthe test calls itBookControllerrealBookService@MockitoBean (a mock)BookRepositoryDataSourcenot createdApiExceptionHandlerPriceJson (real)Everything up to the amber box is real or mocked; everything to its right does not exist in this context.
The diagram shows the boundary of this slice. The controller, the advice and the JSON component are real; the service is a stand-in you control; the repository and the database simply are not there. That boundary is also where this slice’s error comes from.

What goes wrong: you forget the mock

Delete the @MockitoBean line and the context cannot start, because BookController’s constructor asks for a BookService and nothing provides one. This is the real message, produced by MissingMockBeanFailure.java and recorded in 07-slice-failures.txt:
== MissingMockBeanFailure
exception chain: IllegalStateException -> UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
top message    : Failed to load ApplicationContext for
wiring failure : Error creating bean with name 'bookController' defined in file [<module>/target/classes/lab/slices/BookController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'lab.slices.BookService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Read the chain from the bottom up. NoSuchBeanDefinitionException is the real complaint; UnsatisfiedDependencyException says who needed it (bookController, constructor parameter 0); IllegalStateException is only the test framework saying the context did not start. The fix is a @MockitoBean for the type named in the message. Importing the real service would just move the error one level down, because it needs a BookRepository, which this slice does not create — the next section’s failure shows that message.
@MockitoBean is not a Boot annotation. It lives in Spring Framework’s org.springframework.test.context.bean.override.mockito package, which is why it kept working while the Boot-side annotations moved. The old @MockBean does not exist on a Boot 4.1.2 classpath at all; the full list of what moved is in the “Coming from Boot 3” section.
Going deeper: what a mocked-service controller test proves, and what it cannot A @WebMvcTest test with a mocked service proves the parts that belong to the web layer: the URL and HTTP method map to the right method, path variables and parameters are converted, the response is serialised the way you expect, and exceptions become the status codes and bodies you intended. It cannot prove that the service does the right thing, or that the SQL is right — the mock returns whatever you told it to. That is a feature, not a gap: when one of these tests fails, the cause is in the web layer, and it fails in milliseconds. The cost is that a wrong assumption about the service (what it throws, what it returns for no result) is baked into the stub. Keep one end-to-end test for that, as the comparison section shows.

@JsonTest pins down the JSON your API promises

The JSON a class produces is a contract with whoever calls your API, and it is easy to break by renaming a field or changing a serializer. @JsonTest tests exactly that and nothing else: it builds Jackson, any @JacksonComponent, and a helper called JacksonTester. No MVC, no MockMvc, no database. The test below writes a BookView to JSON and reads it back. Source: JsonSliceTest.java.
@JsonTest
class JsonSliceTest {

    @Autowired
        var written = json.write(view);
        String text = written.getJson();
        BookView back = json.parseObject(text);
        assertThat(written).extractingJsonPathStringValue("$.price").isEqualTo("INR 499.00");
        assertThat(written).extractingJsonPathStringValue("$.published_on").isEqualTo("2018-01-06");
        assertThat(back).isEqualTo(view);
The output, from 02-json-slice.txt:
write(view) -> {"isbn":"9780134685991","title":"Effective Java","author":"Joshua Bloch","price":"INR 499.00","published_on":"2018-01-06"}
parseObject(...).price() -> Price[currency=INR, amount=499.00]
The JSON uses published_on because of the @JsonProperty on the record, and INR 499.00 because PriceJson is registered as a component — the slice picked it up by scanning, with no extra configuration. The second line shows the round trip: the deserializer turned the string back into a Price. This cost one failed run while writing the companion module. The first version of the test built the expected Price with new BigDecimal("499") and compared it to the parsed value, which has scale 2; BigDecimal.equals compares scale, so the two were not equal. If your serializer changes the scale of a number, build your expected value with the scale the JSON carries.
Boot 4 renamed two things you will meet here. The annotation for custom serializers is now @JacksonComponent (the old @JsonComponent is not on the classpath), and the tester is JacksonTester, which wraps a Jackson 3 JsonMapper. A Jackson2Tester class still exists in spring-boot-test, but it wraps Jackson 2’s ObjectMapper, and Boot 4 does not put Jackson 2 on the classpath: the JVM could not even load the class to inspect it. Both facts are recorded in 09-package-moves.txt and 11-json-testers.txt.
Single-argument constructor of each tester in spring-boot-test 4.1.2

JacksonTester   wraps tools.jackson.databind.json.JsonMapper
Jackson2Tester  cannot be inspected: com.fasterxml.jackson.databind.ObjectMapper is not on the classpath
Interestingly, @JsonTest did not move. It is still in org.springframework.boot.test.autoconfigure.json, inside spring-boot-test-autoconfigure — the one slice annotation of the four that your Boot 3 import still resolves.
Going deeper: why test JSON separately from the controller You could check the JSON through @WebMvcTest, and the web slice above already does for one book. The reason to test it on its own is cost and precision: no MVC is built (35 bean definitions instead of 126), and a failure points at the serializer and nothing else. It also lets you test shapes the controller never returns today, such as a record used only in a message queue.

@RestClientTest fakes the other end of an HTTP call

IsbnLookupClient calls an ISBN service over HTTP. You do not want a unit test to depend on the network, and you do want to check that your code sends the right request and copes with a 404. @RestClientTest builds the one client class you name, a RestClient.Builder, and a MockRestServiceServer that intercepts its requests, so you describe the server’s answers in the test. Source: RestClientSliceTest.java.
@RestClientTest(IsbnLookupClient.class)
class RestClientSliceTest {

    @Autowired
    IsbnLookupClient client;

    @Autowired
    MockRestServiceServer server;
        server.expect(requestTo("https://isbn.example.test/isbn/9780134685991"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess("""
                        {"isbn":"9780134685991","title":"Effective Java","author":"Joshua Bloch","pages":412}""",
                        APPLICATION_JSON));
        server.expect(requestTo("https://isbn.example.test/isbn/0000000000"))
                .andRespond(withResourceNotFound());

        var found = client.lookup("9780134685991");
        var missing = client.lookup("0000000000");
        server.verify();
The output, from 03-restclient-slice.txt:
lookup(9780134685991) -> Optional[IsbnInfo[isbn=9780134685991, title=Effective Java, author=Joshua Bloch, pages=412]]
lookup(0000000000)    -> Optional.empty
A 200 with a JSON body becomes an IsbnInfo; a 404 becomes Optional.empty, because the client catches HttpClientErrorException.NotFound and turns it into an empty result. The server.verify() call at the end fails the test if a request you expected never happened, which catches a client that quietly stopped calling the service. The base URL comes from application.properties, so the request goes to https://isbn.example.test — a host that does not exist. Nothing leaves the JVM.

What goes wrong: you do not name the client

The annotation takes the client class as an argument. Leave it off and the slice does not go looking for your @Component on its own. This is ClientWithoutComponentsFailure.java and what it threw:
== ClientWithoutComponentsFailure
exception chain: UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
wiring failure : Error creating bean with name 'lab.slices.failing.ClientWithoutComponentsFailure': Unsatisfied dependency expressed through field 'client': No qualifying bean of type 'lab.slices.IsbnLookupClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
The fingerprint of “this slice does not scan that class”. Two lines, always the same shape: exception chain: UnsatisfiedDependencyException -> NoSuchBeanDefinitionException and a message that names a bean type you wrote. The cure is never to widen the slice by hand; it is to name the class in the annotation, mock it, or move the test to the slice that owns that type.
Going deeper: why the client catches only the 404 IsbnLookupClient catches HttpClientErrorException.NotFound and nothing broader. The test proves the 404 case: a 404 becomes an empty result. Any other error status is not caught, so it propagates as an exception — that path is not exercised in this module, so it is reasoning from the code rather than something the run shows. The point of the narrow catch is that “the service does not know this ISBN” and “the service is broken” stay different outcomes. The base URL is read from application.properties, and the expectations in the test repeat it, so changing the property means changing the test.

@DataJpaTest runs your queries against a real database engine and rolls back

Repository methods such as findByAuthorIgnoreCaseOrderByTitle are not Java you wrote — Spring Data builds the query from the method name — so the only way to know they are right is to run them. @DataJpaTest builds the JPA entities, the Spring Data repositories, an in-memory database and a TestEntityManager for setting up rows, and it wraps every test in a transaction that is rolled back at the end. No web layer, no service. Source: DataJpaSliceTest.java.
@DataJpaTest
@TestMethodOrder(MethodOrderer.MethodName.class)
class DataJpaSliceTest {

    @Autowired
    BookRepository repository;

    @Autowired
    TestEntityManager em;

    @Autowired
    DataSource dataSource;
The first test inserts three books and runs the derived query with an author name in the wrong case:
        em.persist(new Book("1", "Zebra", "Ann", BigDecimal.ONE, LocalDate.of(2020, 1, 1)));
        em.persist(new Book("2", "Apple", "ann", BigDecimal.TEN, LocalDate.of(2021, 1, 1)));
        em.persist(new Book("3", "Other", "Bob", BigDecimal.TEN, LocalDate.of(2022, 1, 1)));
        em.flush();

        var books = repository.findByAuthorIgnoreCaseOrderByTitle("ANN");
From 04-datajpa-slice.txt:
findByAuthorIgnoreCaseOrderByTitle("ANN") -> [Apple, Zebra]
DataSource class -> EmbeddedDataSourceProxy
JDBC URL starts with jdbc:h2:mem: -> true
The query is case-insensitive and sorted by title, so “ANN” finds both Ann and ann, with Apple before Zebra. The DataSource is an embedded H2 database. This module has no other database configured, so the run cannot show what happens when one is. Boot also ships @AutoConfigureTestDatabase (in the JDBC test module, see the table in the Boot 3 section), which concerns the test database, but this module does not exercise it. The rollback is easy to take on trust, so the module proves it with two tests that each insert one row and each expect to see exactly one:
    @Test
    void b_firstTestInsertsOneRow() {
        repository.save(new Book("10", "B", "X", BigDecimal.ONE, LocalDate.of(2020, 1, 1)));
        assertThat(repository.count()).isEqualTo(1);
    }

    @Test
    void c_secondTestStillSeesOnlyItsOwnRow() {
        repository.save(new Book("11", "C", "X", BigDecimal.ONE, LocalDate.of(2020, 1, 1)));
        assertThat(repository.count()).isEqualTo(1);
        new Transcript("05-datajpa-rollback.txt")
From 05-datajpa-rollback.txt, written by the second test:
b_firstTestInsertsOneRow   -> count after save = 1
c_secondTestStillSeesOnlyItsOwnRow -> count after save = 1
(the row from b_ was rolled back when its test ended)
If the first test’s row had survived, the second would have counted two. It did not, so each test starts from an empty table.

What goes wrong: an annotation on the application class

This is the trap the earlier section warned about. A very common setup puts @EnableJpaAuditing on the @SpringBootApplication class. Every slice starts from that class, so the web slice — which has no database and therefore no JPA metamodel — fails too. The module cannot annotate its own application class without breaking the working slices, so JpaAuditingInWebSliceFailure.java applies the same annotation through an @Import on a web slice. This is the failure, recorded in 08-jpa-auditing-in-web-slice.txt:
@WebMvcTest + @EnableJpaAuditing (via @Import), BookService mocked
exception chain: IllegalStateException -> BeanCreationException -> BeanCreationException -> IllegalArgumentException
root cause      : JPA metamodel must not be empty
Fix it where the annotation lives, not in the test. Move @EnableJpaAuditing (and @EnableJpaRepositories, and similar) out of the application class and into a separate @Configuration class. A slice only loads that class if you import it, so the web slice stays free of JPA, and the data slice can import it explicitly when it needs auditing. One honest caveat: the module shows the failure and its message, not the fix, so treat the fix as the standard remedy rather than something this run verified.
Going deeper: why an embedded database is both the point and the limit An embedded H2 starts in milliseconds and needs nothing installed, which is why @DataJpaTest defaults to it. The limit is that it is not your production engine: SQL dialect, locking behaviour and unique-index rules can differ, so a query that passes here can still fail on the real database. The mapping and the shape of the query transfer; engine-specific behaviour does not. If you rely on engine-specific features, run those tests against the real engine as well.

Side by side: what each slice actually creates, and what it costs

Each slice test asks its own context which of twelve beans it holds, and a last test merges the answers. Here is the merged table, exactly as 20-slice-matrix.txt wrote it:
bean                                       WebMvcTest JsonTest  RestClientTest  DataJpaTest  SpringBootTest
BookController (@RestController)           yes        no        no              no           yes
ApiExceptionHandler (@ControllerAdvice)    yes        no        no              no           yes
PriceJson (@JacksonComponent)              yes        yes       yes             no           yes
MockMvc                                    yes        no        no              no           yes
JsonMapper (Jackson 3)                     yes        yes       yes             no           yes
BookService (@Service)                     yes        no        no              no           yes
BookRepository (Spring Data)               no         no        no              yes          yes
EntityManagerFactory                       no         no        no              yes          yes
DataSource                                 no         no        no              yes          yes
RestClient.Builder                         no         no        yes             no           yes
IsbnLookupClient (@Component)              no         no        yes             no           yes
AuditLogger (@Component)                   no         no        no              no           yes
bean definitions                           126        35        53              80           251
Which beans exist in which context (green = present) WebMvc Json RestClient DataJpa Full BookController (@RestController) yes no no no yes ApiExceptionHandler (@ControllerAdvice) yes no no no yes PriceJson (@JacksonComponent) yes yes yes no yes MockMvc yes no no no yes JsonMapper (Jackson 3) yes yes yes no yes BookService (@Service) yes no no no yes BookRepository (Spring Data) no no no yes yes EntityManagerFactory no no no yes yes DataSource no no no yes yes RestClient.Builder no no yes no yes IsbnLookupClient (@Component) no no yes no yes AuditLogger (@Component) no no no no yes The columns are @WebMvcTest, @JsonTest, @RestClientTest, @DataJpaTest and @SpringBootTest, in that order.
The same table as a picture. Read it by rows to see where a bean lives, and by columns to see what a slice gives you. Three things stand out. AuditLogger is only in the full context, which is exactly what you want from a canary. BookService exists in the web slice but it is the mock, not the real service. And JsonMapper and PriceJson turn up in the JSON and REST-client slices as well as the web slice, because those slices need Jackson too. The baseline for comparison is a full @SpringBootTest with @AutoConfigureMockMvc, so the test can call the controller. It builds all 251 definitions, and only the remote ISBN service is replaced, because it lives outside the process. It is also the only context in which a real request travels controller → service → repository → database, from 06-full-context.txt:
POST /api/books/import/9780134685991 -> 200 {"isbn":"9780134685991","title":"Effective Java","author":"Joshua Bloch","price":"INR 499.00","published_on":"2024-01-01"}
GET  /api/books?author=joshua bloch   -> 200 [{"isbn":"9780134685991","title":"Effective Java","author":"Joshua Bloch","price":"INR 499.00","published_on":"2024-01-01"}]
Now the cost. The script runs each test class in its own JVM and records the line Spring Boot logs when a test context is ready. These are cold numbers — the first context in a fresh JVM pays for class loading — so read them as indicative, not as a benchmark. From 30-startup-times.txt:
Context startup time per test class (one class per mvn run; indicative, varies between runs)

Started JsonSliceTest in 1.132 seconds
Started RestClientSliceTest in 1.521 seconds
Started WebMvcSliceTest in 2.619 seconds
Started DataJpaSliceTest in 3.173 seconds
Started FullContextTest in 4.344 seconds
On this small app the slices became ready in 1.1 to 3.2 seconds and the full context in 4.3 seconds. The gap grows with the size of the app, because slices skip everything they do not need, so measure your own: run each test class on its own and look for that Started ... in N seconds line.
Going deeper: how the matrix is produced Each slice test calls BeanProbe.record with its own ApplicationContext, which asks the live context for twelve bean types by class and records yes; it also records getBeanDefinitionCount(). The probes are by type, so a mock of BookService counts as a BookService, which is why the web slice shows BookService as present. A final test, ZMatrixTest, runs last (the Surefire run order is alphabetical, set in pom.xml), reads those files from target/probe/, asserts every slice has fewer definitions than the full context, and writes the table.

Coming from Boot 3: the imports that stopped compiling

If you have Spring Boot 3 tests, this is the section that will bite first. Spring Boot 4 split its test support into per-technology modules, and three of the four slice annotations moved with them. Rather than trusting a migration note, the module asks the classpath: PackageMovesTest.java calls Class.forName on the old and the new name of each annotation. Here is the result, from 09-package-moves.txt:
AnnotationBoot 3 locationOn 4.1.2 classpath?Boot 4.1.2 locationOn 4.1.2 classpath?
@WebMvcTestorg.springframework.boot.test.autoconfigure.web.servlet.WebMvcTestnot foundorg.springframework.boot.webmvc.test.autoconfigure.WebMvcTestpresent
@AutoConfigureMockMvcorg.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvcnot foundorg.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvcpresent
@DataJpaTestorg.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestnot foundorg.springframework.boot.data.jpa.test.autoconfigure.DataJpaTestpresent
TestEntityManagerorg.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagernot foundorg.springframework.boot.jpa.test.autoconfigure.TestEntityManagerpresent
@AutoConfigureTestDatabaseorg.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabasenot foundorg.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabasepresent
@RestClientTestorg.springframework.boot.test.autoconfigure.web.client.RestClientTestnot foundorg.springframework.boot.restclient.test.autoconfigure.RestClientTestpresent
@JsonTestorg.springframework.boot.test.autoconfigure.json.JsonTestpresentorg.springframework.boot.test.autoconfigure.json.JsonTestpresent
@MockBeanorg.springframework.boot.test.mock.mockito.MockBeannot foundorg.springframework.test.context.bean.override.mockito.MockitoBeanpresent
@JsonComponentorg.springframework.boot.jackson.JsonComponentnot foundorg.springframework.boot.jackson.JacksonComponentpresent
@SpyBeanorg.springframework.boot.test.mock.mockito.SpyBeannot foundorg.springframework.test.context.bean.override.mockito.MockitoSpyBeanpresent
The table is generated from the committed transcript, so it cannot disagree with it. The three slice annotations that need a technology beyond Jackson moved to new packages named after it (webmvc, data.jpa, restclient), and so did their helpers (@AutoConfigureMockMvc, TestEntityManager, @AutoConfigureTestDatabase). @MockBean was replaced by Spring Framework’s @MockitoBean, and @JsonTest stayed where it was. A Boot 3 test therefore fails at the import line. This is javac’s own text for three Boot 3 style tests in src/broken/, from 10-old-imports-compile-errors.txt:
javac against the Boot 4.1.2 test classpath, sources from src/broken/

OldDataJpaTest.java:3: error: package org.springframework.boot.test.autoconfigure.orm.jpa does not exist
OldDataJpaTest.java:4: error: package org.springframework.boot.test.autoconfigure.orm.jpa does not exist
OldDataJpaTest.java:7: error: cannot find symbol
    symbol: class DataJpaTest
OldDataJpaTest.java:9: error: cannot find symbol
    symbol:   class TestEntityManager
    location: class lab.slices.OldDataJpaTest
OldRestClientTest.java:3: error: package org.springframework.boot.test.autoconfigure.web.client does not exist
OldRestClientTest.java:6: error: cannot find symbol
    symbol: class RestClientTest
OldWebMvcTest.java:3: error: package org.springframework.boot.test.autoconfigure.web.servlet does not exist
OldWebMvcTest.java:4: error: package org.springframework.boot.test.mock.mockito does not exist
OldWebMvcTest.java:7: error: cannot find symbol
    symbol: class WebMvcTest
OldWebMvcTest.java:9: error: cannot find symbol
    symbol:   class MockBean
    location: class lab.slices.OldWebMvcTest

10 errors
The second half of the trap is the dependency. The new annotations live in new jars, and spring-boot-starter-test does not bring them. This is what a project with only that starter gets, from 12-starter-test-only-jars.txt (run by starter-test-only.sh):
Spring Boot 4.1.2: webmvc + data-jpa + restclient starters, and only spring-boot-starter-test for tests
spring-boot jars with 'test' in the name on that classpath:
  spring-boot-starter-test-4.1.2.jar
  spring-boot-test-4.1.2.jar
  spring-boot-test-autoconfigure-4.1.2.jar
That is why the module’s pom.xml adds one test starter per production starter. The block is lines 42 to 62 of pom.xml:
    <!-- Boot 4 splits the test support per technology: one test starter for each starter above -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-webmvc-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-restclient-test</artifactId>
      <scope>test</scope>
    </dependency>
A practical order for the upgrade. Add the three test starters first, then replace the imports one annotation at a time using the table above, then replace @MockBean with @MockitoBean. The compiler will list every file for you, and nothing in a slice test fails silently on a missing import. The same “compare the jars, then fix what the compiler reports” method is used in the JUnit 5 to 6 and Spring Security 6 to 7 guides.
Going deeper: the five failure messages, side by side All five deliberately broken test classes in lab.slices.failing, with the exception chain and the wiring failure each one threw, as written to 07-slice-failures.txt:
Each class in lab.slices.failing is run through the JUnit Launcher; this is what its exception says.
(The 'Failed to load ApplicationContext' line carries a long configuration dump with object hashes; it is cut at the first '['.
 The module directory is shown as <module>.)

== MissingMockBeanFailure
exception chain: IllegalStateException -> UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
top message    : Failed to load ApplicationContext for
wiring failure : Error creating bean with name 'bookController' defined in file [<module>/target/classes/lab/slices/BookController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'lab.slices.BookService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

== ServiceInDataJpaFailure
exception chain: UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
wiring failure : Error creating bean with name 'lab.slices.failing.ServiceInDataJpaFailure': Unsatisfied dependency expressed through field 'service': No qualifying bean of type 'lab.slices.BookService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

== RepositoryInWebMvcFailure
exception chain: UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
wiring failure : Error creating bean with name 'lab.slices.failing.RepositoryInWebMvcFailure': Unsatisfied dependency expressed through field 'repository': No qualifying bean of type 'lab.slices.BookRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

== PlainComponentInJsonFailure
exception chain: UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
wiring failure : Error creating bean with name 'lab.slices.failing.PlainComponentInJsonFailure': Unsatisfied dependency expressed through field 'audit': No qualifying bean of type 'lab.slices.AuditLogger' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

== ClientWithoutComponentsFailure
exception chain: UnsatisfiedDependencyException -> NoSuchBeanDefinitionException
wiring failure : Error creating bean with name 'lab.slices.failing.ClientWithoutComponentsFailure': Unsatisfied dependency expressed through field 'client': No qualifying bean of type 'lab.slices.IsbnLookupClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
  • Run by FailureCaptureTest.java through the JUnit Launcher, so the messages are captured by the build rather than copied from a console. The classes carry the tag failure-demo, which the Surefire configuration in pom.xml excludes from the normal run.

Which slice for which test, and when not to use one

What are you testing? Reach for the smallest context that contains it An endpoint: URL, status code, validation, error JSON @WebMvcTest The JSON shape of one class @JsonTest My code calling someone else’s HTTP API @RestClientTest My queries and entity mappings @DataJpaTest The layers wired together, end to end @SpringBootTest The amber box is the only one that builds the whole application.
Pick the smallest context that contains the thing you are testing. If your question is about an HTTP mapping, a status code, validation or an error body, use the web slice and mock the service. If it is about the JSON shape of one class, use the JSON slice. If it is about your code calling someone else’s API, use the REST-client slice. If it is about a query or an entity mapping, use the data slice. If it is about layers working together, only the full context can answer.
SliceBuildsYou supplyBean definitions
@WebMvcTestMVC, named controllers, advice, Jackson, MockMvc@MockitoBean for services126
@JsonTestJackson, @JacksonComponent, JacksonTesternothing35
@RestClientTestthe named client, RestClient.Builder, MockRestServiceServerthe client class in the annotation53
@DataJpaTestentities, repositories, embedded database, TestEntityManagerrows, per test80
@SpringBootTesteverythingmocks for what is outside the process251
Should you use slices at all? Usually yes, but not for everything. Slices make a test cheap and make its failure local, and they are the right tool for the four jobs above. They also mock the neighbours, so a suite made only of slices can be green while the layers disagree — the web slice trusts your stub of the service, the data slice never sees the web layer. Keep a small number of full-context tests that go through every layer, like the one in this module. And if your controller has no logic and your app starts in a second, a slice per endpoint may be more ceremony than value; that last point is a judgement, not something this run measured.
Going deeper: what this module did not test Not covered, so not claimed: @WebFluxTest (Boot’s slice for reactive web code), security tests with @WithMockUser, how the Spring test context cache behaves when you vary mocks between classes, and testing against a real database engine in a container. Each deserves its own run before you rely on a statement about it.

Further reading

No Comments yet!

This site uses Akismet to reduce spam. Learn how your comment data is processed.