如何在 Spring Boot 中测试组件/bean [英] How to test a component / bean in Spring Boot

查看:80
本文介绍了如何在 Spring Boot 中测试组件/bean的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

要在 Spring Boot 应用程序中测试组件/bean,Spring Boot 文档的测试部分 提供了很多信息和多种方式:@Test@SpringBootTest@WebMvcTest@DataJpaTest 以及许多其他方式.
为什么提供这么多方式?如何决定青睐的方式?
我是否应该将使用 Spring Boot 测试注释(例如 @SpringBootTest@WebMvcTest@DataJpaTest)注释的测试类视为集成测试?

To test a component/bean in a Spring Boot application, the testing part of the Spring Boot documentation provides much information and multiple ways : @Test, @SpringBootTest, @WebMvcTest, @DataJpaTest and still many other ways.
Why provide so many ways ? How decide the way to favor ?
Should I consider as integration tests my test classes annotated with Spring Boot test annotations such as @SpringBootTest, @WebMvcTest, @DataJpaTest ?

PS:我创建这个问题是因为我注意到许多开发人员(甚至有经验的开发人员)没有得到使用注释而不是另一个注释的后果.

PS : I created this question because I noticed that many developers (even experienced) don't get the consequences to use an annotation rather than another.

推荐答案

TL-DR

  • 为组件编写简单的单元测试无需加载 Spring 容器即可直接测试(在本地和 CI 构建中运行它们).

    TL-DR

    • write plain unit tests for components that you can straightly test without loading a Spring container (run them in local and in CI build).

      编写部分集成测试/切片单元测试 用于不加载 Spring 容器就不能直接测试的组件,例如与 JPA、控制器、REST 客户端、JDBC 相关的组件......(在本地和在 CI 构建中)

      write partial integration tests/slicing unit test for components that you cannot straightly test without loading a Spring container such as components related to JPA, controllers, REST clients, JDBC ... (run them in local and in CI build)

      为一些带来价值的高级组件编写一些完整的集成测试(端到端测试)(在 CI 构建中运行它们).

      write some full integration tests (end-to-end tests) for some high-level components where it brings values (run them in CI build).

      • 简单的单元测试(不加载 Spring 容器)
      • 完整的集成测试(加载一个包含所有配置和 bean 的 Spring 容器)
      • 部分集成测试/测试切片(加载配置和 bean 非常受限的 Spring 容器)

      一般情况下,Spring 可以在集成测试中测试任何组件,并且只有某些类型的组件适合进行整体测试(没有容器).
      但请注意,无论有没有 spring,单一测试和集成测试都不是对立的,而是互补的.

      In a general way with Spring any component can be tested in integration tests and only some kinds of components are suitable to be tested unitary(without container).
      But note that with or without spring, unitary and integration tests are not opposed but complementary.

      您识别出要测试的代码没有来自 Spring 容器的任何依赖项,因为组件/方法不使用 Spring 功能来执行其逻辑.
      拿那个 FooService 类:

      You recognize a code to test that doesn't have any dependencies from a Spring container as the component/method doesn't use Spring feature to perform its logical.
      Take that FooService class :

      @Service
      public class FooService{
      
         private FooRepository fooRepository;
         
         public FooService(FooRepository fooRepository){
             this.fooRepository = fooRepository;
         }
      
         public long compute(...){
            List<Foo> foos = fooRepository.findAll(...);
             // core logic
            long result = 
                 foos.stream()
                     .map(Foo::getValue)
                     .filter(v->...)
                     .count();
             return result;
         }
      }
      

      FooService 执行一些不需要 Spring 执行的计算和逻辑.
      事实上,无论有没有容器,compute() 方法都包含我们想要断言的核心逻辑.
      相反,您将很难在没有 Spring 的情况下测试 FooRepository,因为 Spring Boot 会为您配置数据源、JPA 上下文,并检测您的 FooRepository 接口以提供默认实现和其他很多东西.
      测试控制器(rest 或 MVC)也是如此.
      如果没有 Spring,控制器如何绑定到端点?控制器如何在没有 Spring 的情况下解析 HTTP 请求并生成 HTTP 响应?根本做不到.

      FooService performs some computations and logic that don't need Spring to be executed.
      Indeed with or without container the compute() method contains the core logic we want to assert.
      Reversely you will have difficulties to test FooRepository without Spring as Spring Boot configures for you the datasource, the JPA context, and instrument your FooRepository interface to provide to it a default implementation and multiple other things.
      Same thing for testing a controller (rest or MVC).
      How could a controller be bound to an endpoint without Spring? How could the controller parse the HTTP request and generate an HTTP response without Spring? It simply cannot be done.

      在您的应用程序中使用 Spring Boot 并不意味着您需要为您运行的任何测试类加载 Spring 容器.
      当您编写不需要来自 Spring 容器的任何依赖项的测试时,您不必在测试类中使用/加载 Spring.
      您将自己实例化要测试的类,而不是使用 Spring,并在需要时使用模拟库将被测实例与其依赖项隔离.
      这是遵循的方法,因为它速度快并且有利于测试组件的隔离.
      这里如何对上面介绍的 FooService 类进行单元测试.
      您只需要模拟 FooRepository 即可测试 FooService 的逻辑.
      使用 JUnit 5 和 Mockito,测试类可能如下所示:

      Using Spring Boot in your application doesn't mean that you need to load the Spring container for any test class you run.
      As you write a test that doesn't need any dependencies from the Spring container, you don't have to use/load Spring in the test class.
      Instead of using Spring you will instantiate yourself the class to test and if needed use a mock library to isolate the instance under test from its dependencies.
      That is the way to follow because it is fast and favors the isolation of the tested component.
      Here how to unit-test the FooService class presented above.
      You just need to mock FooRepository to be able to test the logic of FooService.
      With JUnit 5 and Mockito the test class could look like :

      import org.mockito.junit.jupiter.MockitoExtension;
      import org.mockito.Mock;
      import org.mockito.Mockito;
      import org.junit.jupiter.api.extension.ExtendWith;
      import org.junit.jupiter.api.Assertions;
      import org.junit.jupiter.api.BeforeEach;
      
      
      @ExtendWith(MockitoExtension.class)
      class FooServiceTest{
      
          FooService fooService;  
      
          @Mock
          FooRepository fooRepository;
      
          @BeforeEach 
          void init{
              fooService = new FooService(fooRepository);
          }
      
          @Test
          void compute(){
              List<Foo> fooData = ...;
              Mockito.when(fooRepository.findAll(...))
                     .thenReturn(fooData);
              long actualResult = fooService.compute(...);
              long expectedResult = ...;
              Assertions.assertEquals(expectedResult, actualResult);
          }
      
      }
      

      2) 编写完整的集成测试

      编写端到端测试需要加载一个容器,其中包含应用程序的整个配置和 bean.
      为了实现这一点 @SpringBootTest 是这样的:

      注释通过创建在您的应用程序中使用的 ApplicationContext 起作用通过 SpringApplication 进行测试

      The annotation works by creating the ApplicationContext used in your tests through SpringApplication

      您可以通过这种方式使用它来测试它而无需任何模拟:

      You can use it in this way to test it without any mock :

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.junit.jupiter.api.Test;
      
      @SpringBootTest
      public class FooTest {
      
         @Autowired
         Foo foo;
      
         @Test
         public void doThat(){
            FooBar fooBar = foo.doThat(...);
            // assertion...
         }    
         
      }
      

      但是如果有意义,您也可以模拟容器的一些 bean :

      But you can also mock some beans of the container if it makes sense :

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.mock.mockito.MockBean;
      import org.springframework.boot.test.context.SpringBootTest;
      import org.junit.jupiter.api.Test;
      import org.mockito.Mockito;
      
      @SpringBootTest
      public class FooTest {
      
         @Autowired
         Foo foo;
      
         @MockBean
         private Bar barDep;
      
         @Test
         public void doThat(){
            Mockito.when(barDep.doThis()).thenReturn(...);
            FooBar fooBar = foo.doThat(...);
            // assertion...
         }    
         
      }
      

      注意模拟的区别,因为您要模拟 Bar 类的普通实例(org.mockito.Mock 注释)和要模拟 Bar bean(org.springframework.boot.test.mock.mockito.MockBean注解).

      Note the difference for mocking as you want to mock a plain instance of a Bar class (org.mockito.Mock annotation)and that you want to mock a Bar bean of the Spring context (org.springframework.boot.test.mock.mockito.MockBean annotation).

      加载一个完整的 spring 上下文需要时间.因此,您应该谨慎使用 @SpringBootTest,因为这可能会使单元测试执行时间非常长,并且通常您不希望强烈减慢开发人员机器上的本地构建和重要的测试反馈使开发人员的测试编写愉快而高效.
      这就是为什么慢"测试一般不在开发者的机器上执行.
      所以你应该让它们成为集成测试(IT 后缀而不是测试类命名中的 Test 后缀)并确保这些只在持续集成构建中执行.
      但是由于 Spring Boot 作用于应用程序中的许多事物(rest 控制器、MVC 控制器、JSON 序列化/反序列化、持久性等等...),您可以编写许多仅在 CI 构建上执行的单元测试,而这不是也可以.
      仅在 CI 构建上执行端到端测试是可以的,但仅在 CI 构建上执行持久性、控制器或 JSON 测试则根本不行.
      实际上,开发人员构建会很快,但缺点是在本地执行的测试只会检测到一小部分可能的回归......
      为了防止这种警告,Spring Boot 提供了一种中间方式:部分集成测试或切片测试(他们称之为):下一点.

      Loading a full spring context takes time. So you should be cautious with @SpringBootTest as this may make unit tests execution to be very long and generally you don't want to strongly slow down the local build on the developer's machine and the test feedback that matters to make the test writing pleasant and efficient for developers.
      That's why "slow" tests are generally not executed on the developer's machines.
      So you should make them integration tests (IT suffix instead of Test suffix in the naming of the test class) and make sure that these are executed only in the continuous integration builds.
      But as Spring Boot acts on many things in your application (rest controllers, MVC controllers, JSON serialization/deserialization, persistence, and so for...) you could write many unit tests that are only executed on the CI builds and that is not fine either.
      Having end-to-end tests executed only on the CI builds is ok but having also persistence, controllers or JSON tests executed only on the CI builds is not ok at all.
      Indeed, the developer build will be fast but as drawback the tests execution in local will detect only a small part of the possible regressions...
      To prevent this caveat, Spring Boot provides an intermediary way : partial integration test or the slice testing (as they call it) : the next point.

      正如在认识一个可以简单测试的测试(没有弹簧)"这一点中所解释的那样,某些组件只能使用正在运行的容器进行测试.
      但是为什么使用 @SpringBootTest 加载应用程序的所有 bean 和配置,而您只需要加载几个特定的​​配置类和 bean 来测试这些组件?
      例如,为什么要加载完整的 Spring JPA 上下文(bean、配置、内存数据库等)来测试控制器部分?
      反过来,为什么要加载与 Spring 控制器关联的所有配置和 bean 来测试 JPA 存储库部分?
      Spring Boot 通过 切片测试功能.
      这些没有简单的单元测试(即没有容器)那么快,但它们确实比加载整个 spring 上下文快得多.所以在本地机器上执行它们通常是可以接受的.
      每个切片测试风格都会加载一组非常有限的自动配置类,您可以根据自己的要求进行修改.

      As explained in the point "Recognizing a test that can be plain tested (without spring))", some components can be tested only with a running container.
      But why using @SpringBootTest that loads all beans and configurations of your application while you would need to load only a few specific configuration classes and beans to test these components?
      For example why loading a full Spring JPA context (beans, configurations, in memory database, and so forth) to test the controller part?
      And reversely why loading all configurations and beans associated to Spring controllers to test the JPA repository part?
      Spring Boot addresses this point with the slice testing feature.
      These are not as much as fast than plain unit tests (that is without container) but these are really much faster than loading a whole spring context. So executing them on the local machine is generally very acceptable.
      Each slice testing flavor loads a very restricted set of auto-configuration classes that you can modify if needed according to your requirements.

      一些常见的切片测试功能:

      测试对象 JSON 序列化和反序列化是否有效正如预期的那样,您可以使用 @JsonTest 注释.

      To test that object JSON serialization and deserialization is working as expected, you can use the @JsonTest annotation.

      • 自动配置的 Spring MVC 测试:@WebMvcTest
      • 要测试 Spring MVC 控制器是否按预期工作,请使用@WebMvcTest 注释.

        To test whether Spring MVC controllers are working as expected, use the @WebMvcTest annotation.

        • 自动配置的 Spring WebFlux 测试:@WebFluxTest
        • 要测试 Spring WebFlux 控制器是否按预期工作,您可以使用 @WebFluxTest 注释.

          To test that Spring WebFlux controllers are working as expected, you can use the @WebFluxTest annotation.

          • 自动配置的数据 JPA 测试:@DataJpaTest
          • 您可以使用 @DataJpaTest 注释来测试 JPA 应用程序.

            You can use the @DataJpaTest annotation to test JPA applications.

            而且您还有许多 Spring Boot 提供给您的其他切片口味.
            请参阅文档的测试部分 以获取更多详细信息.
            请注意,如果您需要定义一组特定的 bean 来加载内置测试切片注释未解决的问题,您还可以创建自己的测试切片注释(https://spring.io/blog/2016/08/30/custom-test-slice-with-spring-boot-1-4).

            And you have still many other slice flavors that Spring Boot provides to you.
            See the testing part of the documentation to get more details.
            Note that if you need to define a specific set of beans to load that the built-in test slice annotations don't address, you can also create your own test slice annotation(https://spring.io/blog/2016/08/30/custom-test-slice-with-spring-boot-1-4).

            几天前,我遇到了一个案例,我会在部分集成中测试一个依赖于几个 bean 的服务 bean,而这些 bean 本身也依赖于其他 bean.我的问题是,由于通常的原因(http 请求和数据库中有大量数据的查询),必须模拟两个深度依赖 bean.
            加载所有 Spring Boot 上下文看起来开销很大,所以我尝试只加载特定的 bean.为了实现这一点,我使用 @SpringBootTest 注释测试类,并指定 classes 属性来定义要加载的配置/beans 类.
            经过多次尝试,我得到了一些似乎有效的东西,但我必须定义要包含的重要 bean/配置列表.
            那真的不整洁也不可维护.
            所以作为更清晰的选择,我选择使用 Spring Boot 2.2 提供的惰性 bean 初始化功能:

            Some days ago, I have encountered a case where I would test in partial integration a service bean that depends on several beans that themselves also depend on other beans. My problem was that two deep dependency beans have to be mocked for usual reasons (http requests and a query with large data in database).
            Loading all the Spring Boot context looked an overhead, so I tried to load only specific beans. To achieve that, I annotation the test class with @SpringBootTest and I specified the classes attribute to define the configuration/beans classes to load.
            After many tries I have gotten something that seemed working but I had to define an important list of beans/configurations to include.
            That was really not neat nor maintainable.
            So as clearer alternative, I chose to use the lazy bean initialization feature provided by Spring Boot 2.2 :

            @SpringBootTest(properties="spring.main.lazy-initialization=true")
            public class MyServiceTest { ...}
            

            这样做的好处是只加载运行时使用的 bean.
            我完全不认为使用该属性必须成为测试类中的规范,但在某些特定的测试用例中,这似乎是正确的方式.

            That has the advantage to load only beans used at runtime.
            I don't think at all that using that property has to be the norm in test classes but in some specific test cases, that appears the right way.

            这篇关于如何在 Spring Boot 中测试组件/bean的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆