TL-DR
-
为组件编写简单的单元测试无需加载 Spring 容器即可直接测试(在本地和 CI 构建中运行它们)。
-
为组件编写部分集成测试/slicing unit test 如果不加载 Spring 容器就无法直接测试,例如与 JPA、控制器、REST 客户端、JDBC 相关的组件...(在本地和 CI 构建)
-
为一些带来价值的高级组件编写一些完整的集成测试(端到端测试)(在 CI 构建中运行它们)。
测试组件的三种主要方法
- 普通单元测试(不加载 Spring 容器)
- 完整的集成测试(加载一个包含所有配置和 bean 的 Spring 容器)
- 部分集成测试/测试切片(加载配置和 bean 非常受限的 Spring 容器)
所有组件都可以用这3种方式测试吗?
一般来说,使用 Spring 的任何组件都可以在集成测试中进行测试,只有某些类型的组件适合进行整体测试(无容器)。
但请注意,无论有没有 spring,单一测试和集成测试并不是对立的,而是互补的。
如何确定一个组件是可以简单测试(不使用 spring)还是只使用 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 是这样的:
注释通过创建您的应用程序中使用的 ApplicationContext 来工作
通过 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 作用于应用程序中的许多事物(rest 控制器、MVC 控制器、JSON 序列化/反序列化、持久性等等),您可以编写许多仅在 CI 构建上执行的单元测试,而事实并非如此也可以。
仅在 CI 构建上执行端到端测试是可以的,但仅在 CI 构建上执行持久性、控制器或 JSON 测试根本不行。
事实上,开发人员构建速度会很快,但缺点是在本地执行的测试只能检测到一小部分可能的回归...
为了避免这种警告,Spring Boot 提供了一种中间方式:部分集成测试或切片测试(他们称之为):下一点。
3)通过切片测试编写专注于特定层或关注点的部分集成测试
正如“识别可以进行简单测试(没有弹簧)的测试”中所述,某些组件只能使用正在运行的容器进行测试。
但是,为什么要使用 @SpringBootTest 来加载应用程序的所有 bean 和配置,而您只需要加载一些特定的配置类和 bean 来测试这些组件呢?
例如,为什么要加载完整的 Spring JPA 上下文(bean、配置、内存数据库等)来测试控制器部分?
反过来,为什么要加载与 Spring 控制器关联的所有配置和 bean 来测试 JPA 存储库部分?
Spring Boot 使用 slice testing feature 解决了这一点。
这些不如普通单元测试(即没有容器)快,但它们确实比加载整个 spring 上下文快得多。
所以在本地机器上执行它们通常是非常可接受的。
每个切片测试风格都会加载一组非常有限的自动配置类,您可以根据需要对其进行修改。
一些常见的切片测试功能:
测试对象 JSON 序列化和反序列化是否正常工作
正如预期的那样,您可以使用@JsonTest 注解。
要测试 Spring MVC 控制器是否按预期工作,请使用
@WebMvcTest 注释。
要测试 Spring WebFlux 控制器是否按预期工作,您
可以使用@WebFluxTest注解。
您可以使用@DataJpaTest 注解来测试JPA 应用程序。
Spring Boot 还为您提供了许多其他切片风味。
请参阅the testing part of the documentation 了解更多详情。
请注意,如果您需要定义一组特定的 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 属性来定义要加载的配置/bean 类。
经过多次尝试,我得到了一些似乎有效的东西,但我必须定义一个重要的 bean/配置列表。
那真的很不整洁,也不可维护。
所以作为更清晰的选择,我选择使用 Spring Boot 2.2 提供的惰性 bean 初始化功能:
@SpringBootTest(properties="spring.main.lazy-initialization=true")
public class MyServiceTest { ...}
这样做的好处是只加载运行时使用的 bean。
我根本不认为使用该属性必须成为测试类中的规范,但在某些特定的测试用例中,这似乎是正确的方式。