Skip to content
Advertisement

Why component scanning does not work for Spring Boot unit tests?

The service class FooServiceImpl is annotated with @Service aka @Component which makes it eligible for autowiring. Why this class is not being picked up and autowired during unit tests?

@Service
public class FooServiceImpl implements FooService {
    @Override
    public String reverse(String bar) {
        return new StringBuilder(bar).reverse().toString();
    }
}

@RunWith(SpringRunner.class)
//@SpringBootTest
public class FooServiceTest {
    @Autowired
    private FooService fooService;
    @Test
    public void reverseStringShouldReverseAnyString() {
        String reverse = fooService.reverse("hello");
        assertThat(reverse).isEqualTo("olleh");
    }
}

The test failed to load application context,

2018-02-08T10:58:42,385 INFO    Neither @ContextConfiguration nor @ContextHierarchy found for test class [io.github.thenilesh.service.impl.FooServiceTest], using DelegatingSmartContextLoader
2018-02-08T10:58:42,393 INFO    Could not detect default resource locations for test class [io.github.thenilesh.service.impl.FooServiceTest]: no resource found for suffixes {-context.xml}.
2018-02-08T10:58:42,394 INFO    Could not detect default configuration classes for test class [io.github.thenilesh.service.impl.FooServiceTest]: FooServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2018-02-08T10:58:42,432 INFO    Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, (...)org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener]
2018-02-08T10:58:42,448 INFO    Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@f0ea28, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@16efaab,(...)org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@9604d9]
2018-02-08T10:58:42,521 INFO    Refreshing org.springframework.context.support.GenericApplicationContext@173f9fc: startup date [Thu Feb 08 10:58:42 IST 2018]; root of context hierarchy
2018-02-08T10:58:42,606 INFO    JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2018-02-08T10:58:42,666 ERROR    Caught exception while allowing TestExecutionListener [org.springframework.test.context.support.DependencyInjectionTestExecutionListener@19aaa5] to prepare test instance [io.github.thenilesh.service.impl.FooServiceTest@57f43]
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'io.github.thenilesh.service.impl.FooServiceTest': Unsatisfied dependency expressed through field 'fooService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'io.github.thenilesh.service.FooService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588) ~[spring-beans-4.3.13.RELEASE.jar:4.3.13.RELEASE]
    . . . 
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192) [.cp/:?]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'io.github.thenilesh.service.FooService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1493) ~[spring-beans-4.3.13.RELEASE.jar:4.3.13.RELEASE]
    ... 28 more
2018-02-08T10:58:42,698 INFO    Closing org.springframework.context.support.GenericApplicationContext@173f9fc: startup date [Thu Feb 08 10:58:42 IST 2018]; root of context hierarchy

Full stack trace

If test class is annotated with @SpringBootTest then it creates whole application context including database connection and a lot of unrelated beans which obviously not needed for this unit test(It won’t be unit test then!). What is expected is that only beans on which FooService depends should be instantiated, except which are mocked, with @MockBean.

Advertisement

Answer

You should use @SpringBootTest(classes=FooServiceImpl.class).

As it’s mentioned on Annotation Type SpringBootTest:

public abstract Class<?>[] classes

The annotated classes to use for loading an ApplicationContext. Can also be specified using @ContextConfiguration(classes=…). If no explicit classes are defined the test will look for nested @Configuration classes, before falling back to a SpringBootConfiguration search.

Returns: the annotated classes used to load the application context See Also: ContextConfiguration.classes()

Default: {}

This would load only necessary class. If don’t specify, it may load a database configuration and other stuff which would make your test slower.

On the other hand, if you want really want unit test, you can test this code without Spring – then @RunWith(SpringRunner.class) and @SpringBootTest annotations are not necessary. You can test FooServiceImpl instance. If you have Autowired/injected properties or services, you set them via setters, constructors or mock with Mockito.

Advertisement