繁体   English   中英

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

[英]How to test a component / bean in Spring Boot

为了测试在Spring启动应用程序的组件/豆, 春天启动文档的测试部分提供了很多的信息和多种方式: @Test@SpringBootTest@WebMvcTest@DataJpaTest仍然很多其他方式。
为什么提供这么多方式? 如何决定青睐的方式?
我是否应该将使用 Spring Boot 测试注释(例如@SpringBootTest@WebMvcTest@DataJpaTest注释的测试类视为集成测试?

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

TL-DR

  • 为组件编写简单的单元测试,您可以在不加载 Spring 容器的情况下直接测试(在本地和 CI 构建中运行它们)。

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

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


测试组件的 3 种主要方法

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

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

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

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

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

@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 响应? 它根本无法做到。

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

在您的应用程序中使用 Spring Boot 并不意味着您需要为您运行的任何测试类加载 Spring 容器。
当您编写不需要来自 Spring 容器的任何依赖项的测试时,您不必在测试类中使用/加载 Spring。
您将自己实例化要测试的类,而不是使用 Spring,并在需要时使用模拟库将被测实例与其依赖项隔离。
这是遵循的方法,因为它速度快并且有利于测试组件的隔离。
这里如何对上面介绍的FooService类进行单元测试。
您只需要模拟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的方法是:

注释的工作原理是通过 SpringApplication 创建在测试中使用的 ApplicationContext

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

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( org.springframework.boot.test.mock.mockito.MockBean注释)。

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

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

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

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

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

要测试该对象 JSON 序列化和反序列化是否按预期工作,您可以使用 @JsonTest 批注。

要测试 Spring MVC 控制器是否按预期工作,请使用@WebMvcTest注释。

要测试 Spring WebFlux 控制器是否按预期工作,您可以使用@WebFluxTest注释。

您可以使用@DataJpaTest批注来测试 JPA 应用程序。

您还有许多 Spring Boot 提供给您的其他切片口味。
请参阅文档的测试部分以获取更多详细信息。
请注意,如果您需要定义一组特定的 bean 来加载内置测试切片注释未解决的问题,您还可以创建自己的测试切片注释( 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。 为了实现这一点,我使用@SpringBootTest注释测试类,并指定classes属性来定义要加载的配置/beans 类。
经过多次尝试,我得到了一些似乎有效的东西,但我必须定义要包含的重要 bean/配置列表。
那真的不整洁也不可维护。
因此,作为更清晰的选择,我选择使用 Spring Boot 2.2 提供的惰性 bean 初始化功能:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM