代码之家  ›  专栏  ›  技术社区  ›  Nilesh

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

  •  4
  • Nilesh  · 技术社区  · 7 年前

    服务类 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,这些bean显然不是本单元测试所需要的(不会是 单元 然后测试!)。我们所期望的是,只有 FooService 应使用 @MockBean .

    3 回复  |  直到 7 年前
        1
  •  5
  •   Justinas Jakavonis    3 年前

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

    正如上面提到的 Annotation Type SpringBootTest :

    公共抽象类<&燃气轮机;[]类

    用于加载ApplicationContext的带注释的类。可以 也可以使用@ContextConfiguration(类=…)指定。如果否 定义了显式类,测试将查找嵌套类 @配置类,然后返回到 SpringBootConfiguration搜索。

    返回值: 用于加载应用程序上下文的带注释类另请参见: 上下文配置。类别()

    违约: {}

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

    另一方面,如果您真的想要进行单元测试,那么可以在没有Spring的情况下测试此代码-然后 @RunWith(SpringRunner.class) @SpringBootTest 不需要注释。您可以测试 FooServiceImpl 例子如果你有 Autowired /注入的属性或服务,可以通过setter、constructor或mock Mockito .

        2
  •  2
  •   Raj    6 年前

    我不得不稍微改变一下,解决一个类似的问题。我想分享这方面的细节,认为这可能会给那些遇到类似问题的人提供选择。

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

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

    设计之前

    1.com。Java语言服务ServiceOneImpl

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

    2.com。Java语言服务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语言服务一ServiceOneImpl

    @服务
    公共类ServiceOneImpl实现ServiceOne{
    @自动连线
    私人RepositoryOne RepositoryOne;
    @自动连线
    私人服务二服务二;
    @自动连线
    私人MapperOne MapperOne;
    }
    

    2.com。Java语言服务二ServiceTwoImpl

    @服务
    公共类ServiceTwoImpl实现ServiceTwo{
    @自动连线
    私人回购2回购2;
    }
    

    3、ServiceOneIntegrationTest

    @RunWith(SpringRunner.class)
    @数据JPATEST
    @组件扫描({“com.java.service”,“com.java.mapper”})
    公共类ServiceOneIntegrationTest{
    

    4、ServiceTwoIntegrationTest。Java语言

    @RunWith(SpringRunner.class)
    @DataJpaTest
    @ComponentScan({"com.java.service.two"}) // CHANGE in the packages
    public class ServiceTwoIntegrationTest {
    
        3
  •  0
  •   ekem chitsiga    7 年前

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

    如果您想要一个真正的集成测试,那么需要在测试类上使用@SpringBootTest注释。如果不指定classes属性,它将加载@SpringBootApplication注释类。这会导致加载生产组件,如db连接。

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

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