要在 Spring Boot 应用程序中测试组件/bean,the testing part of the Spring Boot documentation 提供了很多信息和多种方法:@Test
、 @SpringBootTest
、 @WebMvcTest
、 @DataJpaTest
以及许多其他方式。
为什么提供这么多方式?
如何决定青睐的方式?
我是否应该将使用 Spring Boot 测试注释(例如 @SpringBootTest
、 @WebMvcTest
、 @DataJpaTest
)注释的测试类视为集成测试?
PS:我创建这个问题是因为我注意到许多开发人员(甚至是有经验的)没有得到使用注释而不是另一个注释的后果。
最佳答案
TL-DR
测试组件的 3 种主要方法
是否可以通过这 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 解决了这一点。
这些没有简单的单元测试(没有容器)那么快,但它们确实比加载整个上下文快得多。 所以在本地机器上执行它们通常是可以接受的 。
每个切片测试风格都会加载一组非常有限的自动配置类,您可以根据需要进行修改。
一些常见的切片测试功能:
To test that object JSON serialization and deserialization is working as expected, you can use the @JsonTest annotation.
To test whether Spring MVC controllers are working as expected, use the
@WebMvcTest
annotation.
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/