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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/51789880/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 00:15:24  来源:igfitidea点击:

How to test a component / bean in Spring Boot

javaunit-testingspring-bootjunitmockito

提问by davidxxx

To test a component/bean in a Spring Boot application, the testing part of the Spring Boot documentationprovides much information and multiple ways : @Test, @SpringBootTest, @WebMvcTest, @DataJpaTestand 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?

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

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.

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

回答by davidxxx

TL-DR

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).

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

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

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

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

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



3 main ways to test a component

测试组件的 3 种主要方法

  • plain unit test (doesn't load a Spring container)
  • full integration test (load a Spring container with all configuration and beans)
  • partial integration test/ test slicing (load a Spring container with very restricted configurations and beans)
  • 普通单元测试(不加载 Spring 容器)
  • 完整的集成测试(加载一个包含所有配置和 bean 的 Spring 容器)
  • 部分集成测试/测试切片(加载具有非常受限的配置和 bean 的 Spring 容器)

Can all components be tested in these 3 ways ?

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

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,unitary 和 integration 测试都不是对立的,而是互补的。

How to determine if a component can be plain tested (without spring) or only tested with Spring?

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

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.
In the previous example, FooServiceperforms 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 FooRepositorywithout Spring as Spring Boot configures for you the datasource, the JPA context, and instrument your FooRepositoryinterface 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 容器的任何依赖项,因为组件/方法不使用 Spring 功能来执行其逻辑。
在前面的示例中,FooService执行一些不需要 Spring 执行的计算和逻辑。
实际上,无论有没有容器,该compute()方法都包含我们想要断言的核心逻辑。
相反,如果FooRepository没有 Spring,您将难以进行测试,因为 Spring Boot 会为您配置数据源、JPA 上下文,并检测您的 FooRepository接口以向其提供默认实现和其他多项内容。
测试控制器(rest 或 MVC)也是如此。
如果没有 Spring,控制器如何绑定到端点?控制器如何在没有 Spring 的情况下解析 HTTP 请求并生成 HTTP 响应?它根本无法做到。

1)Writing a plain unit test

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

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 haveto 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.
For example, a FooServiceannotated as Spring service that performs some computations and that rely on FooRepositoryto retrieve some data can be tested without Spring :

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

You can mock FooRepositoryand unit test the logic of FooService.
With JUnit 5 and Mockito the test class could look like :

您可以模拟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)Writing a full integration test

2)编写完整的集成测试

Writing an end-to-end test requires to load a container with the whole configuration and beans of the application.
To achieve that @SpringBootTestis the way :

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

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

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

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...
   }    

}

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

但是,如果有意义,您也可以模拟容器的一些 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...
   }    

}

Note the difference for mocking as you want to mock a plain instance of a Barclass (org.mockito.Mockannotation)and that you want to mock a Barbean of the Spring context (org.springframework.boot.test.mock.mockito.MockBeanannotation).

请注意模拟的区别,因为您想模拟一个Bar类的普通实例(org.mockito.Mock注释)和您想模拟BarSpring 上下文的bean(org.springframework.boot.test.mock.mockito.MockBean注释)。

Full integration tests have to be executed by the CI builds

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

Loading a full spring context takes time. So you should be cautious with @SpringBootTestas 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 (ITsuffix instead of Testsuffix 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.

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

3)Writing a partial integration test focusing on a specific layer or concern thanks to slice testing

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

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 @SpringBootTestthat will load 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 unitary test a controller ?
And reversely why loading all configurations and beans associated to Spring controllers to unitary test a JPA repository?
Spring Boot addresses this point with the slice testing feature.
These are not as much as fast than a plain unit tests (without container) but these are really much faster than loading the whole 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.

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

Some common slice testing features :

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

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

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

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

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

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

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

You can use the @DataJpaTestannotation to test JPA applications.

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

And you have still many other slice flavors that Spring Boot provides to you.
See the testing part of the documentationto 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).

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

4)Writing a partial integration test focusing on specific beans thanks to lazy bean initialization

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

Some days ago, I have encountered a case where I would test in integration a service bean that depends on several beans that themselves also depend on other beans. My problem was that two 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, as a first try, I specified @SpringBootTestat the class level and I specified the classesproperty 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 :

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

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

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.

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