> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# TestRestTemplate's NoSuchBeanDefinitionException in Spring Boot 4
- URL: https://www.ggorantala.dev/spring-boot-4-testresttemplate-nosuchbeandefinitionexception/
- Published: 2026-10-02T05:13:25.000Z
- Updated: 2026-10-02T05:24:03.000Z
- Description: Boot 4 quietly stopped auto-wiring TestRestTemplate and MockMvc in @SpringBootTest, and the @MockBean-to-@MockitoBean rename hides a sneakier trap of its own.
- Author: Gopi Gorantala
- Tags: spring-boot, spring-boot-errors, nosuchbeandefinitionexception, spring-boot-4, testing, mockitobean, migration

## The test run that stopped in its tracks

I went into the Boot 4 migration assuming the test module would be the boring part. Rename `@MockBean` to `@MockitoBean`, fix a couple of imports, done. I was wrong about that, and it cost me more than a day to find out exactly how wrong, in two completely unrelated ways at once.

Here's the first one. A test that had run green for two years, untouched, on Boot 3.5:

```log
org.springframework.boot.test.context.SpringBootTest$WebEnvironment RANDOM_PORT

java.lang.IllegalStateException: Failed to load ApplicationContext for
[WebMergedContextConfiguration@4f8a3c1 testClass = com.example.repro.GreetingControllerRestTemplateTests,
locations = [], classes = [com.example.repro.ReproApplication], ...]

	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(...)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(...)
	...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with
name 'com.example.repro.GreetingControllerRestTemplateTests': Unsatisfied dependency expressed through
field 'restTemplate': No qualifying bean of type 'org.springframework.boot.resttestclient.TestRestTemplate'
available: expected at least 1 bean which qualifies as autowire candidate
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type
'org.springframework.boot.resttestclient.TestRestTemplate' available: expected at least 1 bean which
qualifies as autowire candidate
```

No `APPLICATION FAILED TO START` banner. No `FailureAnalyzer`. Just JUnit reporting a context load failure, because this isn't `SpringApplication.run()` blowing up. It's `TestContextManager` failing to build the `ApplicationContext` a test needs before the test method even gets to run. That distinction matters more than it looks like it should, and I'll come back to it.

Versions this run was pinned to: Spring Boot 4.1.1, Spring Framework 7.0.9, Java 21, JUnit 5.12, Maven. Everything here also applies to 4.0.0 through 4.0.8; the mechanism hasn't moved since 4.0, only patch numbers have.

## Before you blame your service layer

The bean it can't find isn't yours. `org.springframework.boot.resttestclient.TestRestTemplate` is a Boot class, in a Boot package, and the reason it's missing has nothing to do with anything you wrote. So before you go anywhere near your own configuration, check two things.

First, is the class even on the classpath. Run `./mvnw dependency:tree -Dincludes=org.springframework.boot:spring-boot-resttestclient` (Maven Central is unreachable for me while writing this, so I can't paste live tree output, but the command is correct against the BOM either way). If it prints nothing, you don't have the jar, and no annotation will save you. That's a different, earlier failure, and it shows up as a compile error, not this one.

Second, if the class does resolve, ask what actually got registered:

```bash
curl -s localhost:PORT/actuator/beans | grep -i "resttestclient\|testResttemplate\|MockMvc"
```

You won't get anywhere with this one, because the context never finishes loading. Actuator has nothing to answer with. That absence is itself the diagnostic: a bean that's missing because a condition didn't match still lets the context start; a bean that's missing because nothing on your test class ever asked Boot to register it stops the context cold, at exactly the field that wanted it.

## Break it the way the migration breaks it

**What you need:** Java 21, Maven 3.9+, Spring Boot 4.1.1\. No database, no Docker, no network.

`start.spring.io` wasn't reachable for me today either, so here's the Initializr call and the exact dependency block side by side. Use whichever actually loads for you.

```bash
curl https://start.spring.io/starter.zip \
  -d type=maven-project -d bootVersion=4.1.1 -d javaVersion=21 \
  -d groupId=com.example -d artifactId=repro -d name=repro \
  -d packageName=com.example.repro -d dependencies=webmvc \
  -o repro.zip && unzip repro.zip -d repro && cd repro
```

If that 404s, add this to `pom.xml` yourself. This is the exact set the reproduction needs, nothing more:

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
```

Deliberately **not** added yet: `spring-boot-starter-webmvc-test`. That's the fix, and skipping it is what makes the failure happen.

```java
package com.example.repro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ReproApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReproApplication.class, args);
    }
}
```

```java
package com.example.repro;

public interface GreetingService {
    String greet(String name);
}
```

```java
package com.example.repro;

import org.springframework.stereotype.Service;

@Service
public class RealGreetingService implements GreetingService {

    private volatile int callCount;

    @Override
    public String greet(String name) {
        this.callCount++;
        return "Hello, " + name;
    }

    public int getCallCount() {
        return this.callCount;
    }
}
```

```java
package com.example.repro;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

    private final GreetingService greetingService;

    public GreetingController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @GetMapping("/greet")
    public String greet(@RequestParam String name) {
        return this.greetingService.greet(name);
    }
}
```

Now the first test, which is the one that reproduces Section 1 exactly:

```java
package com.example.repro;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class GreetingControllerRestTemplateTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void greetsByName() {
        String body = this.restTemplate.getForObject("/greet?name=Gopi", String.class);
        assertThat(body).isEqualTo("Hello, Gopi");
    }
}
```

```log
Step: mvn -Dtest=GreetingControllerRestTemplateTests test
Expected: BUILD FAILURE. The failure isn't in the test method, it's in
"initializationError" / context load, and the exception chain is the
one at the top of this article, word for word down to the field name.
```

Confirmation you hit the right one: the failure is a context-loading `IllegalStateException`, not an assertion failure, and it never gets as far as printing anything from `greetsByName()` itself. If you instead see an assertion failure with an actual HTTP response body, your `TestRestTemplate` already wired up fine and you're chasing a different bug.

**The fix, first pass:** add the missing starter and scope it to test.

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc-test</artifactId>
    <scope>test</scope>
</dependency>
```

Then add the annotation Boot now asks for explicitly:

```java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate
class GreetingControllerRestTemplateTests {
    // unchanged
}
```

Run it again. It passes. Now the second scenario, which won't fail at all, and that's the point.

```java
package com.example.repro;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@TestConfiguration
@MockitoBean(types = GreetingService.class)
public class SharedGreetingMockConfig {
}
```

```java
package com.example.repro;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@SpringBootTest
@Import(SharedGreetingMockConfig.class)
class GreetingServiceSharedMockTests {

    @Autowired
    private RealGreetingService realGreetingService;

    @Autowired
    private GreetingService greetingService;

    @Test
    void mockNeverGetsInTheWay() {
        given(greetingService.greet("Gopi")).willReturn("mocked");

        // This passes on Boot 3.5. On Boot 4.1.1 it also "passes",
        // but for the wrong reason, and that's what to look for.
        assertThat(Mockito.mockingDetails(this.greetingService).isMock())
            .as("greetingService should be a Mockito mock, injected via the shared @TestConfiguration")
            .isFalse(); // <- confirms the mock never took effect
        assertThat(this.realGreetingService.getCallCount()).isZero();
    }
}
```

```log
Step: mvn -Dtest=GreetingServiceSharedMockTests test
Expected: green, silently. isMock() on the injected greetingService
returns false. The real RealGreetingService is what got wired, and
willReturn("mocked") was stubbing a bean nobody ever autowired.
```

Nothing here throws. That's the whole reason it took me a day longer than the first one. A failing test is a lead; a passing test that's lying to you isn't.

## The short version

Two unrelated things changed about how `@SpringBootTest` builds a context in Boot 4, and they both hide behind a clean compile. Boot no longer registers `TestRestTemplate`, `MockMvc`, or a reactive `WebClient` for you just because your test looks like it wants one. You now say so, with `@AutoConfigureTestRestTemplate` or `@AutoConfigureMockMvc`, and you bring the jar that contains them. Separately, `@MockitoBean`, the direct replacement for the removed `@MockBean`, only looks at the test class itself and its enclosing classes. It does not look inside `@Configuration` classes you `@Import` to share mock setup across several test classes, which `@MockBean` did. The rename compiles. The sharing pattern doesn't carry over, and nothing tells you that.

## How @MockitoBean actually finds what to override

`@MockBean` and `@MockitoBean` sound like the same feature with a new name. Structurally they're closer to opposites, and the difference is exactly why one supports `@Configuration` classes and the other doesn't.

`MockitoPostProcessor` (the class behind `@MockBean`, deprecated at 3.4.0 and gone by 4.0) is a `BeanFactoryPostProcessor`. Its `postProcessBeanFactory` doesn't just look at your test class. It calls `getConfigurationClasses(beanFactory)`, which walks every bean definition already registered in the factory and keeps the ones carrying the internal attribute `ConfigurationClassPostProcessor` stamps onto anything it processed as `@Configuration`. Then it re-parses each of those classes for `@MockBean`/`@SpyBean`. By the time this runs, Spring has already registered bean definitions from your `@SpringBootApplication` class, every `@Import`, every auto-configuration. Any `@Configuration` class among them, no matter how it got there, gets scanned. That's the whole trick behind putting `@MockBean` on a shared `@TestConfiguration` and `@Import`\-ing it into a dozen test classes: it doesn't matter *how* the class entered the context, only that it did.

`@MockitoBean` works the other way around. `BeanOverrideContextCustomizerFactory.createContextCustomizer` calls `BeanOverrideHandler.findAllHandlers(testClass)`, and that method's own Javadoc says exactly what it does: build the handler list "from `@BeanOverride` fields in the test class and in its type hierarchy," plus the enclosing-class hierarchy for `@Nested` tests. It never touches the bean factory. It never sees what got registered. It walks the *Java* class hierarchy of the one class JUnit handed it. Full stop. A `@Configuration` class sitting off to the side, reachable only through `@Import` or `@ContextConfiguration(classes = ...)`, is invisible to it, because nothing about being `@Import`\-ed makes it part of the test class's type hierarchy.

Old mechanism, late: after the context is assembled, look at everything that got in. New mechanism, early: before the context exists, look only at the one class you were handed. (`@MockitoBean` also runs before real singletons are created, through its own `BeanFactoryPostProcessor`, `BeanOverrideBeanFactoryPostProcessor`; it's the same "early" habit, just one that only knows about handlers discovered from the test class, never from what Spring itself assembles.) That's the whole article's mechanism in one sentence, and it's also, as far as I can tell, not written down anywhere except the Boot 4.0 migration guide's one line: "the new Mockito annotations cannot be used within `@Configuration` classes." The guide states it. It doesn't say why. Source does.

TestRestTemplate's absence has a plainer story, but it's still not obvious from the outside. `spring-boot-starter-test` in 4.1.1 depends on exactly two things it always did (`spring-boot-test` and `spring-boot-test-autoconfigure`), and nothing else changed there. What changed is that `TestRestTemplateContextCustomizerFactory`, the class that used to auto-register a `TestRestTemplate` whenever `webEnvironment` was `RANDOM_PORT` or `DEFINED_PORT`, doesn't exist in 4.1.1 at all. Not deprecated. Gone, the same way `@MockBean` is gone. In its place: `TestRestTemplateTestAutoConfiguration`, a normal `@AutoConfiguration` class, `@ConditionalOnMissingBean`, that only runs when `@AutoConfigureTestRestTemplate` (`@since 4.0.0`, and it says so right in the Javadoc) imports it. `@AutoConfigureMockMvc` got the identical treatment, moved wholesale into its own module, `spring-boot-webmvc-test`, alongside `MockMvcAutoConfiguration`. Auto-registration became explicit configuration, on purpose, for both of them, in the same release.

## What actually fixes it, and what it costs

For `TestRestTemplate` and `MockMvc` together, one dependency covers both, because `spring-boot-starter-webmvc-test` pulls in `spring-boot-webmvc-test` *and* `spring-boot-resttestclient`:

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webmvc-test</artifactId>
    <scope>test</scope>
</dependency>
```

Then annotate whichever slice you're actually exercising. Use `@AutoConfigureTestRestTemplate` for the HTTP-round-trip style test, `@AutoConfigureMockMvc` for the mock-dispatcher style, both if a class does both. Full-fat `@SpringBootTest` classes that never inject either of these beans need no change at all; this only bites the ones that do.

If you're on WebFlux instead of MVC, the equivalent is `spring-boot-starter-restclient-test` for `RestTestClient`, via `@AutoConfigureRestTestClient`. Same shape, different starter, and it does **not** also pull in `TestRestTemplate`, because that class lives specifically in `spring-boot-resttestclient`, not `spring-boot-restclient-test`. Mixing those two up is an easy way to fix half the problem and not notice.

For the sharing pattern, there's no annotation that restores the old behavior, and I don't think there should be. A mechanism that scans every `@Configuration` class in your context for test doubles is a strange thing to want once you say it out loud. The honest fixes:

- Move the `@MockitoBean` fields onto the actual test classes. More typing, no magic.
- Or write one composed annotation (`@MockitoBean` is meta-annotation-friendly) and put *that* on each test class: `@Retention(RUNTIME) @MockitoBean(types = GreetingService.class) @interface MockGreetingService {}`. Slightly more ceremony than the shared-`@Configuration` trick, considerably more honest about what each test is doing.

What each one costs: field-per-test is more lines in more files. The composed annotation adds one extra file to your test sources, forever, for every group of beans you mock together. Neither costs you a silently-real dependency call in production-shaped tests, which is what the thing you're replacing was quietly risking.

**Not a fix:** leaving `@MockitoBean` on the shared `@Configuration` class and adding `@Import` in more places to "make sure it's picked up." It won't be. `@Import` changes what's in the context; it doesn't put anything into the test class's type hierarchy, which is the only place `findAllHandlers` looks.

## Structuring test mocks so this can't happen quietly

If a group of tests genuinely need the same three or four mocks, the composed-annotation approach above is the design to standardize on going in, not a patch you reach for after getting bitten. Name it for what it replaces, `@MockDownstreamPaymentClients` rather than `@SharedTestMocks`, so a reviewer sees the annotation and knows exactly what's fake in that test without opening a second file.

For the `TestRestTemplate`/`MockMvc` half, the better default is deciding up front which slice a test belongs to. A test that only needs `MockMvc` wants `@WebMvcTest`, not full `@SpringBootTest` plus `@AutoConfigureMockMvc` bolted on. `@WebMvcTest` already imports `MockMvcAutoConfiguration` for you, because `WebMvcTestContextBootstrapper` sets that up as part of the slice, not as an afterthought. Reach for `@AutoConfigureMockMvc` on a full `@SpringBootTest` specifically when you need the whole context *and* a mock dispatcher, which is a real combination, just a less common one than people write by default.

## Which versions bite, and the test that catches the silent one

Everything above is Boot 4.0.0 and later: 4.0.0, every 4.0.x patch, and 4.1.0/4.1.1\. Boot 3.5.16 and earlier still auto-register `TestRestTemplate` and `MockMvc`, and `@MockBean` still works there too, deprecated but functional (deprecated *since* 3.4.0, for what its own Javadoc calls "removal in 3.6.0", a version number that never shipped because the next major got renamed to 4.0 along the way). If you're mid-migration with modules on both sides of that line, expect this exact pair of surprises the moment each module crosses it, not before.

The `TestRestTemplate` gap is the easy one to guard against, because it fails loudly: any CI run that includes the affected tests catches it immediately, every time, with the exact stack trace above.

The `@MockitoBean`\-on-`@Configuration` gap won't fail on its own, which is what makes it worth one extra assertion rather than trusting a green build. Drop this into a shared test base class or a JUnit extension, once, for any test that relies on a shared mock configuration:

```java
@BeforeEach
void verifyMocksAreActuallyMocks(@Autowired GreetingService greetingService) {
    assertThat(Mockito.mockingDetails(greetingService).isMock())
        .as("greetingService must be a Mockito mock; check it isn't declared on a @Configuration class")
        .isTrue();
}
```

Cheap, specific, and it fails exactly where the real risk is, instead of downstream in a network call your test environment happened to survive.

## The five-second summary

`@MockBean` is gone in Boot 4, not deprecated-and-lingering. The package doesn't exist. `@MockitoBean` is the replacement, but it only sees mocks declared on the test class itself (and its enclosing classes), never on a `@Configuration` class you `@Import` to share them, because the discovery mechanism changed from "scan everything Spring assembled" to "walk this one class's hierarchy." Separately, `@SpringBootTest` stopped auto-registering `TestRestTemplate` and `MockMvc`. Add `spring-boot-starter-webmvc-test` plus `@AutoConfigureTestRestTemplate`/`@AutoConfigureMockMvc` explicitly. The `TestRestTemplate` failure is loud and CI catches it for free. The shared-mock failure is quiet, and a one-line `isMock()` assertion in a base test class is the difference between finding it in review and finding it in a postmortem.