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

时间:2018-08-10 15:38:10

标签: 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:之所以创建此问题,是因为我注意到许多开发人员(甚至经验丰富的开发人员)使用注解而不是使用注解并没有带来后果。

1 个答案:

答案 0 :(得分:22)

TL-DR

  • 为不需加载Spring容器即可直接测试的组件编写普通的单元测试(在本地和CI构建中运行它们)。
  • 为不装载Spring容器就无法直接测试的组件编写部分集成测试/ slicing unit test ,例如与JPA,控制器,REST客户端,JDBC等相关的组件。 (在本地和CI构建中运行它们)

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


测试组件的三种主要方法

  • 普通单元测试(不加载Spring容器)
  • 全面集成测试(加载具有所有配置和bean的Spring容器)
  • 部分集成测试/测试切片(将具有严格限制的配置和bean的Spring容器加载)

可以通过这三种方式测试所有组件吗?

使用Spring的一般方法可以在集成测试中测试任何组件,并且只有某些类型的组件适合进行整体测试(无容器)。
但是请注意,无论有没有弹簧,单一测试和集成测试都不是相反的,而是互补的。

编写普通单元测试

在应用程序中使用Spring Boot并不意味着您需要为运行的任何测试类加载Spring容器。
在编写不需要来自Spring容器的任何依赖项的测试时,您没有在测试类中使用/加载Spring。
代替使用Spring,您将自己实例化要测试的类,并在需要时使用模拟库将受测实例与其依赖项隔离。
之所以采用这种方法,是因为它速度快并且有利于隔离测试的组件。
例如,可以在没有Spring的情况下测试一个注解为Spring服务的FooService,它执行一些计算并且依靠FooRepository来检索一些数据:

@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);
    }

}

如何确定某个组件是可以简单测试(不带弹簧)还是只能使用Spring进行测试?

您识别出要测试的代码,该代码与Spring容器没有任何依赖关系,因为组件/方法不使用Spring功能来执行其逻辑。
在上一个示例中,FooService执行一些不需要执行Spring的计算和逻辑。
实际上,无论有没有容器,compute()方法都包含我们要声明的核心逻辑。
相反,如果没有Spring,您将很难测试FooRepository,因为Spring Boot为您配置数据源,JPA上下文,并检测FooRepository接口以为其提供默认实现和其他多种功能。
测试控制器(静止或MVC)的方法相同。
没有Spring,如何将控制器绑定到端点?在没有Spring的情况下,控制器如何解析HTTP请求并生成HTTP响应?根本做不到。

编写完整的集成测试

编写端到端测试需要使用应用程序的整个配置和bean加载容器。
实现@SpringBootTest的方法是:

  

该注释通过创建在您的   通过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( org.springframework.boot.test.mock.mockito.MockBean注释)。

完全集成测试必须由CI版本执行

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

编写针对特定层或关注点的部分集成测试(切片测试)

如“识别可以简单测试(无弹簧)的测试)”所述,某些组件只能在运行的容器中进行测试。
但是,为什么要使用@SpringBootTest来加载应用程序的所有bean和配置,而只需要加载一些特定的配置类和bean来测试这些组件呢?
例如,为什么要加载完整的Spring JPA上下文(bean,配置,内存数据库等)以统一测试控制器?
相反,为什么要加载与Spring控制器关联的所有配置和Bean,以统一测试JPA存储库?
Spring Boot用slice testing feature解决了这一点。
这些速度没有普通单元测试(没有容器)快,但实际上比加载整个上下文快得多。 因此通常可以在本地计算机上执行它们
每个切片测试版本都会加载一组非常有限的自动配置类,您可以根据需要根据需要进行修改。

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

  

要测试对象JSON序列化和反序列化是否正常   如预期的那样,您可以使用@JsonTest批注。

  

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

  

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

  

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

Spring Boot还为您提供了许多其他切片功能。
请参阅the testing part of the documentation,以获取更多详细信息。