为什么组件扫描不适用于Spring Boot单元测试?

时间:2018-02-08 07:14:35

标签: java spring unit-testing spring-boot integration-testing

服务类FooServiceImpl使用@Service又名@Component进行注释,这使其有资格进行自动装配。为什么在单元测试期间没有拾取和自动装配此类?

@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");
    }
}

测试无法加载应用程序上下文,

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

如果测试类使用@SpringBootTest进行注释,那么它会创建整个应用程序上下文,包括数据库连接和许多不相关的bean,这显然不需要进行此单元测试(它不会 unit < / em>测试然后!)。预期的是,只有FooService依赖的bean应该被实例化,除非被模拟,@MockBean

3 个答案:

答案 0 :(得分:3)

您应该使用@SpringBootTest(classes=FooServiceImpl.class)

正如Annotation Type SpringBootTest上提到的那样:

  

public abstract Class [] classes

     

用于加载ApplicationContext的带注释的类。能够   也可以使用@ContextConfiguration(classes = ...)指定。如果不   定义了显式类,测试将查找嵌套   @Configuration类,在回落之前   SpringBootConfiguration搜索。

     

返回:       用于加载应用程序上下文的带注释的类另请参见:       ContextConfiguration.classes()

     

默认:       {}

这只会加载必要的类。如果没有指定,它可能会加载数据库配置和其他会使测试速度变慢的东西。

另一方面,如果您真的想要单元测试,可以在没有Spring的情况下测试此代码 - 然后@RunWith(SpringRunner.class)@SpringBootTest注释不是必需的。您可以测试FooServiceImpl实例。如果您拥有Autowired /注入的属性或服务,则可以通过setter,constructors或模拟Mockito来设置它们。

答案 1 :(得分:1)

单元测试应该单独测试组件。您甚至不需要使用Spring Test上下文框架进行单元测试。您可以使用模拟框架(如Mockito,JMock或EasyMock)来隔离组件中的依赖项并验证期望。

如果您想要进行真正的集成测试,那么您需要在测试类上使用@SpringBootTest注释。如果您不指定classes属性,则会加载@SpringBootApplication带注释的类。这导致生成组件,如数据库连接被加载。

要消除这些,请定义一个单独的测试配置类,例如定义嵌入式数据库而不是生产数据库

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

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

答案 2 :(得分:1)

我不得不稍微解决一下类似的问题。想分享其中的细节,以为遇到类似问题的人可以选择。

我想编写集成测试,其中仅加载必要的依赖项,而不是所有应用程序依赖项。因此,我选择使用@DataJpaTest而不是@SpringBootTest。而且,我还必须包括@ComponentScan才能解析@Service bean。但是,当ServiceOne开始使用另一个软件包中的mapper bean时,我必须指定要@ComponentScan加载的特定软件包。令人惊讶的是,我什至必须为第二项服务执行此操作,该服务不会自动连接此映射器。我不喜欢它,因为它给读者留下了这种服务实际上依赖于那个映射器的印象。因此,我意识到需要进一步调整服务的包结构,以更准确地表示依赖关系。

总而言之,@ DataJpaTest + @ ComponentScan与包名称的组合可以代替@SpringBootTest使用,仅用于加载特定于图层的依赖项。这甚至可以帮助我们微调设计以更准确地表示您的依赖关系。

之前设计

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 {

微调程序包名称后

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 {