When working for the first time with Spring, you might encounter several no qualifying bean exceptions when you try to start your application. The stack trace is quite long for such error scenarios and it might become frustrating as your application doesn't start. The same is true whenever your test works with a Spring Test Context. Most of the time, the root cause for this is easy to fix. With this article, I'm covering a set of common pitfalls of why Spring is unable to resolve a bean when writing tests for a Spring Boot application.
The following examples are using JUnit 5 together with Spring Boot > 2.2.0.RELEASE
The Spring Bean is not part of the sliced Spring Context
A common scenario is the following: We have a Spring MVC controller endpoint that has several collaborators. During runtime, Spring injects these collaborators and the endpoint can do its work.
Let's take the PublicController
as a simplified example for this common use case:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @RestController @RequestMapping("/public") public class PublicController { private final UserService userService; public PublicController(UserService userService) { this.userService = userService; } // ... endpoint definitions } |
When testing this controller, we can use @WebMvcTest to test the controller with MockMvc. In addition to this, we get a sliced Spring Context that contains only relevant Spring Beans for this test.
If we would now write the following test setup and try to inject both an instance of MockMvc
and the UserService
…
1 2 3 4 5 6 7 8 9 10 11 12 13 | @WebMvcTest(PublicController.class) class PublicControllerTest { @Autowired private MockMvc mockMvc; @Autowired private UserService userService; // ... tests } |
… any test inside this test class will fail with the following (reduced) stack trace:
1 2 3 4 5 6 7 8 9 10 | Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'publicController' defined in file [/home/rieckpil/development/git/sample-project/target/classes/de/rieckpil/blog/PublicController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'de.rieckpil.learning.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} ... 68 more Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'de.rieckpil.learning.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {} ... 87 more |
These stack traces can be very long and might be overwhelming especially if you are new to Spring and Spring Boot. What's important here is the NoSuchBeanDefinitionException
which causes the UnsatisfiedDependencyException
. During context initialization, Spring is trying to instantiate the PublicController
. As this controller has one collaborator (UserService
), Spring tries to resolve this dependency by injecting it from its context.
But there is no available bean of type UserService
inside the test context as we are using @WebMvcTest
which only populates MVC components. This ignores populating any non-relevant @Service
or @Component
classes.
In the end, this whole chain results in an IllegalStateException
as Spring is not able to load the ApplicationContext
. This will fail our test before we can even execute any test logic.
When testing different slices of our application in isolation, we can fix this unresolved bean exception by providing a mocked bean.
1 2 3 4 5 6 7 8 9 10 11 | @WebMvcTest(PublicController.class) class PublicControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; // ... } |
This will place a bean of the UserService
inside the Spring Test Context. However, this bean doesn't represent the actual completion and is a mocked version of it. Please note that this @MockBean is different from Mockito's @Mock.
We can also add a real bean to our context, which we'll see in the last section of this article.
No qualifying bean because of untriggered auto-configuration
Another common error scenario is that we expect a specific auto-configuration mechanism to trigger, but it doesn't for our test setup.
A good example is the auto-configuration of the WebTestClient
or RestTestTemplate
which only happens when we start the embedded Servlet container during a test.
Using @SpringBootTest
without any further configuration will use a mocked Servlet environment. That's why the following test:
1 2 3 4 5 6 7 8 9 10 11 12 | @SpringBootTest class ApplicationIT { @Autowired private WebTestClient webTestClient; @Test void shouldReturn200ForPublicEndpoint() { // ... } } |
… fails as the WebTestclient
is not resolvable:
1 2 3 4 5 6 | org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'de.rieckpil.blog.ApplicationIT': Unsatisfied dependency expressed through field 'webTestClient'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.test.web.reactive.server.WebTestClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} |
The fix for this is simple:
1 2 3 4 5 6 7 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationIT { @Autowired private WebTestClient webTestClient; } |
If you are using IntelliJ IDEA, you'll also get hints before runtime if you'll be able to inject a specific bean for your test. This is quite helpful if we forget e.g. to use @Serivce
or @Component
on top of our classes.
However, there are also false-positives where IDEA thinks we are unable to autowire a bean during tests. This usually happens for scenarios where we programmatically register beans or for more advanced setups.
Spring Boot also provides several meta-annotations to enable auto-configuration explicitly:
@AutoConfigureTestDatabase
auto-configures aDataSource
using an embedded database by default@AutoConfigureCache
to auto-configure a testCacheManager
@AutoConfigureMockRestServiceService
to auto-configure aMockRestServivceServer
to test aRestTemplate
usage- etc.
We are writing a unit test without a Spring Context
Let's take a look at the next possible pitfall when testing our application. This time we want to write a unit test to verify the UserService
. This class has one collaborator (the EntityManager
) and takes care of creating new users:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Service public class UserService { private final EntityManager entityManager; public UserService(EntityManager entityManager) { this.entityManager = entityManager; } public void create(String username) { // create the user } } |
Now when it comes to testing, developers sometimes mix concepts of plain unit testing and writing tests with support from Spring and Spring Boot. This might end up in the following (wrong) test setup.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // wrong test setup, won't work @ExtendWith(MockitoExtension.class) class UserServiceTest { @Autowired private EntityManager entityManager; private UserService userService; @Test void shouldCreateUser() { this.userService = new UserService(entityManager); // possible NullPointerExceptions as the entityManager is null this.userService.create("duke"); } } |
As we are writing our application with Spring Boot, one might think that we are able to inject our beans anywhere. This is not the case here. For the test above no Spring Test Context is created at all and the entityManager
field is null. We also don't register the SpringExtension
here that takes care of injecting the beans for our test.
The correct way in this example would be to only rely on tools that JUnit 5 and Mockito provide.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import javax.persistence.EntityManager; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private EntityManager entityManager; @InjectMocks private UserService userService; @Test void shouldCreateUser() { // test logic } } |
The import section on top of the class is a great indicator to verify that we are not using anything from Spring or Spring Boot for our unit tests.
In this context, it's also important to understand the difference between @Mock and @MockBean.
The Spring Boot main class defines logic and has collaborators
Sometimes projects define Spring Beans or startup logic inside the Spring Boot main entry point class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @SpringBootApplication public class Application implements CommandLineRunner { private final DataSource dataSource; public Application(DataSource dataSource) { this.dataSource = dataSource; } public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Override public void run(String... args) throws Exception { System.out.println("Connection to database successful: " + dataSource.getConnection().getMetaData().getDatabaseProductName()); } } |
The logic above will always be triggered whenever we use a Spring Boot test slice annotation like @WebMvcTest
or @DataJpaTest
. We would see failing web layer tests because Spring is unable to load the ApplicationContext
as it can't resolve the DataSource
bean. That's an overhead we can avoid and we should keep our Spring Boot entry point class as empty as possible.
We can outsource our bean definitions to dedicated @Configuration
classes. Otherwise, all your test that load an ApplicationContext
would have to satisfy the collaborators of your main class.
The same is true for any startup logic that we might want to define at this point.
Adding any Spring Bean to our Spring Test Context
We now saw how we can fix common pitfalls when Spring is unable to resolve a bean. Up until this point we used @MockBean
to place a mocked version of a Spring Bean inside the Spring Test Context. However, there might be scenarios where we don't want a mocked instance and rather a real one. Let's see how we can achieve this.
For demonstration purposes, we'll enrich the PublicController
and add an additional collaborator: MeterRegistry
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @RestController @RequestMapping("/public") public class PublicController { private final UserService userService; private final MeterRegistry meterRegistry; public PublicController(UserService userService, MeterRegistry meterRegistry) { this.userService = userService; this.meterRegistry = meterRegistry; } // endpoint definitions } |
Coming back to the same test we already saw before, we now have to decide what to do with both collaborators. By default, they are not part of the Spring Test Context, as @WebMvcTest
only populates relevant Spring MVC components (speak Spring Beans).
Mocking the collaborator with @MockBean
is always a valid option, but this time we want to add a real MeterRegistry
instance to our Spring Test Context.
To achieve this, we can make use of Spring's @TestConfiguration
annotation and create a nested static class that defines beans. For our example, it's only one as we want to still mock the UserService
bean.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @WebMvcTest(PublicController.class) class PublicControllerTest { @Autowired private MockMvc mockMvc; @Autowired private MeterRegistry meterRegistry; @MockBean private UserService userService; @TestConfiguration static class TestConfig { @Bean public MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); } } } |
With this setup, our test is now able to load the Spring Test Context and can inject all collaborators to our PublicController
. This solution gives us a lot of control as we can decide for every collaborator to either mock it or provide the actual implementation.
We can even outsource this test configuration to a dedicated class …
1 2 3 4 5 6 7 8 9 | @TestConfiguration public class DefaultRegistryConfig { @Bean public MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); } } |
… to reuse it for other tests with @Import
:
1 2 3 4 5 6 7 | @WebMvcTest(PublicController.class) @Import(DefaultRegistryConfig.class) class PublicControllerTest { // no nested @TestConfiguration class needed and we can reuse it } |
This approach allows us to add the real Spring Beans to our test context and e.g. test our web layer in combination with its real collaborator. In general, we should still favor mocking any interaction to the outside (speak to a collaborator) whenever we want to test our classes in isolation.
Have fun fixing your unresolved (no qualifying) Spring Bean exceptions,
Philip