【问题标题】:Why component scanning does not work for Spring Boot unit tests?为什么组件扫描不适用于 Spring Boot 单元测试?
【发布时间】:2018-07-18 16:15:57
【问题描述】:

服务类 FooServiceImpl 使用 @Service aka @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 测试然后!)。预期的是,只有 FooService 所依赖的 bean 应该被实例化,除了那些被模拟的,@MockBean

【问题讨论】:

  • 你试过@SpringBootTest(classes=FooServiceImpl.class)吗?
  • 您需要上下文这一事实已经使其成为集成测试。如果你想要一个单元测试,只需创建一个存储库的实例,模拟依赖关系并完成。只要你需要一个上下文,它就是一个集成测试。
  • @Justas 您的评论应该是一个答案,以及“在单元测试中对 Spring Boot 组件扫描的解释”

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


【解决方案1】:

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

正如Annotation Type SpringBootTest 中提到的:

公共抽象类>[] 类

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

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

默认值: {}

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

另一方面,如果你真的想要单元测试,你可以在没有 Spring 的情况下测试这段代码——那么 @RunWith(SpringRunner.class)@SpringBootTest 注释就不是必需的。您可以测试FooServiceImpl 实例。如果您有Autowired/injected 属性或服务,您可以通过设置器、构造器或使用Mockito 模拟来设置它们。

【讨论】:

    【解决方案2】:

    我必须解决一个稍有变化的类似问题。想分享其中的细节,认为它可能会给遇到类似问题的人提供选择。

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

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

    之前的设计

    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 {
    

    【讨论】:

      【解决方案3】:

      单元测试应该单独测试一个组件。您甚至不需要使用 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
      }
      

      【讨论】:

        猜你喜欢
        • 2016-11-13
        • 1970-01-01
        • 2018-07-22
        • 2015-12-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多