简体   繁体   中英

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 .

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

As it 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 .

I had to solve a similar problem with a slight variation. Thought to share the details of that, thinking it might give choices to those who hit upon similar issues.

I wanted to write integration tests with only necessary dependencies loaded up instead of all of the app dependencies. So I chose to use @DataJpaTest , instead of @SpringBootTest . And, I had to include @ComponentScan too to parse the @Service beans. However, the moment ServiceOne started using a mapper bean from another package, I had to specify the specific packages to be loaded with @ComponentScan . Surprisingly, I even had to do this for the second service that does not auto-wire this mapper. I did not like that because it leaves the impression to the reader that this service depends on that mapper when it is actually not. So I realized that the package structure for services needs to be fine-tuned further to represent the dependencies more accurately.

To summarise, instead of @SpringBootTest, a combination of @DataJpaTest+@ComponentScan with package names can use to load just the layer-specific dependencies. This might even help us to fine-tune the design to represent your dependencies more accurately.

Design before

1. com.java.service.ServiceOneImpl

@Service
public class ServiceOneImpl implements ServiceOne {   
  @Autowired
  private RepositoryOne repositoryOne;    
  @Autowired
  private ServiceTwo serviceTwo;      
  @Autowired
  private MapperOne mapperOne;
}

2. com.java.service.ServiceTwoImpl

@Service
public class ServiceTwoImpl implements ServiceTwo {   
  @Autowired
  private RepositoryTwo repositoryTwo;    
}

3. ServiceOneIntegrationTest

@RunWith(SpringRunner.class)
@DataJpaTest
@ComponentScan({"com.java.service","com.java.mapper"})
public class ServiceOneIntegrationTest {

4. ServiceTwoIntegrationTest.java

@RunWith(SpringRunner.class)
@DataJpaTest
@ComponentScan({"com.java.service","com.java.mapper"})
public class ServiceTwoIntegrationTest {

After fine-tuning the package names

1. com.java.service.one.ServiceOneImpl

@Service
public class ServiceOneImpl implements ServiceOne {   
  @Autowired
  private RepositoryOne repositoryOne;    
  @Autowired
  private ServiceTwo serviceTwo;      
  @Autowired
  private MapperOne mapperOne;
}

2. com.java.service.two.ServiceTwoImpl

@Service
public class ServiceTwoImpl implements ServiceTwo {   
  @Autowired
  private RepositoryTwo repositoryTwo;    
}

3. ServiceOneIntegrationTest

@RunWith(SpringRunner.class)
@DataJpaTest
@ComponentScan({"com.java.service","com.java.mapper"})
public class ServiceOneIntegrationTest {

4. ServiceTwoIntegrationTest.java

@RunWith(SpringRunner.class)
@DataJpaTest
@ComponentScan({"com.java.service.two"}) // CHANGE in the packages
public class ServiceTwoIntegrationTest {

A unit test should test a component in isolation. You don`t even need to use the Spring Test context framework for a unit test. You can using mocking frameworks such as Mockito, JMock or EasyMock to isolate the dependencies in your component and verify the expectations.

If you want a true integration test then you need to use the @SpringBootTest annotation on your test class. If you dont specify the classes attribute it loads the @SpringBootApplication annotated class. This results in production components like db connections being loaded.

To eliminate these define a separate test configuration class which for example defines an embedded database instead of the production one

@SpringBootTest(classes = TestConfiguration.class)
public class ServiceFooTest{
}

@Configuration
@Import(SomeProductionConfiguration.class)
public class TestConfiguration{
   //test specific components
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM