java - 如何在 Spring Boot 中测试组件/bean

标签 java unit-testing spring-boot junit mockito

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

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

最佳答案

TL-DR

  • 为组件 编写简单的单元测试,您可以直接测试而无需加载 Spring 容器 (在本地和 CI 构建中运行它们)。
  • 编写部分集成测试/slicing unit test 组件 不加载Spring 容器就不能直接测试 ,例如 Controller ,与 JPA 相关的 Controller ,在 JPA 中运行 REST 客户端,......在 JPA 中运行 REST2141214214
  • 为一些带来值(value)的高级组件编写一些完整的集成测试(端到端测试)(在 CI 构建中运行它们)。


  • 测试组件的 3 种主要方法
  • 普通单元测试(不加载 Spring 容器)
  • 完整集成测试(加载一个包含所有配置和 bean 的 Spring 容器)
  • 部分集成测试/测试切片(加载具有非常受限配置和 bean 的 Spring 容器)

  • 是否可以通过这 3 种方式测试所有组件?

    在 Spring 的通用方式中,任何组件都可以在集成测试中进行测试,并且只有某些类型的组件适合进行整体测试(没有容器)。
    但请注意,无论有没有 spring,unitary 和 integration 测试都不是对立的,而是互补的。

    如何确定组件是否可以进行简单测试(没有 Spring )或仅使用 Spring 进行测试?

    您认识到要测试的代码没有来自 Spring 容器的任何依赖项,因为组件/方法不使用 Spring 功能来执行其逻辑。
    在前面的示例中,FooService 执行一些不需要 Spring 执行的计算和逻辑。
    实际上,无论有没有容器,compute() 方法都包含我们想要断言的核心逻辑。
    相反,您将很难在没有 Spring 的情况下测试 FooRepository,因为 Spring Boot 会为您配置数据源、JPA 上下文,并检测您的 FooRepository 接口(interface)以向其提供默认实现和其他多项内容。
    测试 Controller (rest 或 MVC)也是如此。
    如果没有 Spring, Controller 如何绑定(bind)到端点? Controller 如何在没有 Spring 的情况下解析 HTTP 请求并生成 HTTP 响应?它根本无法做到。

    1)编写一个简单的单元测试

    在您的应用程序中使用 Spring Boot 并不意味着您需要为您运行的任何测试类加载 Spring 容器。
    当您编写不需要来自 Spring 容器的任何依赖项的测试时, 您没有 在测试类中使用/加载 Spring。
    您将自己实例化要测试的类,而不是使用 Spring,并在需要时使用模拟库将被测实例与其依赖项隔离。
    这是遵循的方法,因为它速度快并且有利于测试组件的隔离。
    例如,注释为 Spring 服务的 FooService 执行一些计算并依赖于 FooRepository 来检索一些数据,可以在没有 Spring 的情况下进行测试:
    @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;
       }
    }
    

    您可以模拟 FooRepository 并对 FooService 的逻辑进行单元测试。
    使用 JUnit 5 和 Mockito,测试类可能如下所示:
    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 是这样的:

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



    您可以通过这种方式使用它来测试它而无需任何模拟:
    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:
    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 注释)和您想模拟 Spring 上下文的 Bar bean(0x2513413224 annotation)。

    完整的集成测试必须由 CI 构建执行

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

    3)由于切片测试,编写专注于特定层或关注点的部分集成测试

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

    一些常见的切片测试功能:
  • Auto-configured JSON Tests : @JsonTest

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


  • Auto-configured Spring MVC Tests : @WebMvcTest

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


  • Auto-configured Spring WebFlux Tests : @WebFluxTest

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


  • @SpringBootTest

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



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

    4)由于懒惰的bean初始化,编写了一个专注于特定bean的部分集成测试

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

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

    关于java - 如何在 Spring Boot 中测试组件/bean,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51789880/

    相关文章:

    java.io.FileNotFoundException :/home/ec2-user/src/main/resources/keystore. jks(没有这样的文件或目录)

    java - 根据 Spring Boot 中的 Accept-Language 检索语言环境

    java - 使 API 中的某些方法仅对特定包可用 - Java

    java - 如何在 onbackpressed() 时刷新 fragment 中的 ListView

    javascript - 无法让 chai.spy.on 工作

    c# - 验证属性从未使用 Moq 设置

    reactjs - 在 Jest 中 stub I18next useTranslation 钩子(Hook)不会触发 toHaveBeenCalled

    java - Spring Boot使用pathvariable从url获取id

    java - 从 Vert.x 发起多个出站调用

    java - 如何动态设置jtable内的jcheckbox