如何在Spring Boot中测试一个组件/bean
如何在Spring Boot中测试组件/bean
TL-DR
- 对于那些不需要加载Spring容器就可以直接测试的组件,编写纯粹的单元测试(在本地和CI构建中运行)。
- 对于那些只能在加载Spring容器后测试的组件,如与JPA、控制器、REST客户端、JDBC相关的组件,编写部分集成测试/测试切片(在本地和CI构建中运行)。
- 对于一些高级组件,如果有价值的话,编写一些完整的集成测试(端到端测试)(只在CI构建中运行)。
3种主要的组件测试方法
- 纯粹的单元测试(不加载Spring容器)
- 完整的集成测试(加载带有所有配置和bean的Spring容器)
- 部分集成测试/测试切片(加载具有非常受限制的配置和bean的Spring容器)
所有组件都可以用这3种方法进行测试吗?
通常情况下,使用Spring可以对任何组件进行集成测试,只有某些类型的组件适合进行单元测试(无需容器)。
但是请注意,无论有没有Spring,单元测试和集成测试并不相互对立,而是互补的。
如何确定一个组件是否可以进行纯粹测试(不使用Spring)还是仅能使用Spring进行测试?
你可以通过识别一个测试代码是否没有依赖于Spring容器来确定。如果组件/方法不使用Spring的功能来执行逻辑,则表示它没有与Spring容器的任何依赖关系。
例如,上面的FooService类就是一个可以进行纯粹测试的示例。它的compute()方法包含了我们想要验证的核心逻辑。
相反,如果要在没有Spring的情况下测试FooRepository将会有困难,因为Spring Boot为您配置了数据源、JPA上下文,并为您的FooRepository接口提供默认实现和其他多个东西。
控制器(REST或MVC)也是同样的道理。控制器如何在没有Spring的情况下绑定到端点?控制器如何解析HTTP请求并生成HTTP响应?这是不可能的。
1)编写纯粹的单元测试
在应用程序中使用Spring Boot并不意味着您需要为运行的每个测试类加载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;
(MockitoExtension.class)
class FooServiceTest{
FooService fooService;
FooRepository fooRepository;
void init{
fooService = new FooService(fooRepository);
}
void compute(){
List
Mockito.when(fooRepository.findAll(...))
.thenReturn(fooData);
long actualResult = fooService.compute(...);
long expectedResult = ...;
Assertions.assertEquals(expectedResult, actualResult);
}
}
2)编写完整的集成测试
编写端到端测试需要加载一个包含应用程序所有配置和bean的容器。
为了实现这一点,可以使用@SpringBootTest注解来测试,而不需要任何模拟:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
public class FooTest {
Foo foo;
public void doThat(){
FooBar fooBar = foo.doThat(...);
// 进行断言...
}
}
如果有必要,您还可以模拟容器中的一些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;
public class FooTest {
Foo foo;
private Bar barDep;
public void doThat(){
Mockito.when(barDep.doThis()).thenReturn(...);
FooBar fooBar = foo.doThat(...);
// 进行断言...
}
}
完整的集成测试必须由CI构建执行
加载完整的Spring上下文需要时间。因此,您应该谨慎使用@SpringBootTest,因为这可能会导致单元测试执行时间过长。通常情况下,您不希望严重减慢开发人员的本地构建速度,并且有关测试的反馈对于使编写测试对开发人员来说愉快和高效非常重要。
因此,通常不会在开发人员的机器上执行“慢速”测试。
因此,您应该将它们作为集成测试(命名时使用IT后缀而不是Test后缀)进行,并确保这些测试只在持续集成构建中执行。
但是,由于Spring Boot对应用程序的许多方面(如REST控制器、MVC控制器、JSON序列化/反序列化、持久化等)都有影响,您可能会编写许多只在CI构建上执行的单元测试,这也不是很好。
只在CI构建上执行端到端测试是可以接受的,但是只在CI构建上执行持久化、控制器或JSON测试则是不可接受的。
事实上,开发人员构建将会很快,但作为缺点,本地运行的测试将只能检测到可能出现的一小部分回归问题。
为了避免这个问题,Spring Boot提供了一种中间方法:部分集成测试或切片测试(如下一点所述)。
3)编写部分集成测试,专注于特定层或关注点,使用切片测试
如前面的“识别可以进行纯粹测试(不使用Spring)的测试”的部分所解释的,某些组件只能在运行容器后进行测试。
但是,为什么要加载所有bean和配置而不是只加载少量特定的配置类和bean来测试这些组件呢?
例如,为什么要加载完整的Spring JPA上下文(bean、配置、内存数据库等)来测试控制器部分呢?
反过来,为什么要加载与Spring控制器相关的所有配置和bean来测试JPA存储库部分呢?
Spring Boot通过切片测试功能解决了这个问题。
这些切片测试不像纯粹的单元测试那样快速,但是它们比加载整个Spring上下文要快得多。
因此,通常情况下,将它们在本地机器上执行是非常可接受的。
每个切片测试类型加载了一组非常受限制的自动配置类,如果需要,可以根据您的要求进行修改。
一些常见的切片测试特性包括:
- 自动配置的JSON测试:用于测试对象的JSON序列化和反序列化是否按预期工作。
- 自动配置的Spring MVC测试:用于测试Spring MVC控制器是否按预期工作。
- 自动配置的Spring WebFlux测试:用于测试Spring WebFlux控制器是否按预期工作。
- 自动配置的JPA测试:用于测试JPA应用程序。
Spring Boot还提供了许多其他切片类型。
请参阅文档中的测试部分以获取更多详细信息。
请注意,如果您需要定义一个特定的要加载的bean集合,而内置的测试切片注解无法满足您的需求,您还可以创建自己的测试切片注解。
4)编写仅关注特定bean的部分集成测试,利用延迟bean初始化
几天前,我遇到了一个问题,我想对一个依赖于多个其他bean的服务bean进行部分集成测试,而这些其他bean本身也依赖于其他bean。
我的问题是,由于通常的原因(HTTP请求和大型数据库中的查询),我需要模拟两个深层依赖bean。
加载整个Spring Boot上下文看起来有点多余,所以我尝试只加载特定的bean。
为此,我在测试类上注释了@SpringBootTest,并使用classes属性指定要加载的配置/bean类。
经过多次尝试,我似乎得到了一个看起来有效的解决方案,但是我不得不定义一个重要的bean/配置列表来包含。
这样做既不整洁,也不易于维护。
因此,作为更清晰的替代方案,我选择使用Spring Boot 2.2提供的延迟bean初始化功能:
(properties="spring.main.lazy-initialization=true")
public class MyServiceTest { ...}
这样做的好处是只加载运行时使用的bean。
我不认为在测试类中使用该属性应该成为规范,但在某些特定的测试用例中,这似乎是正确的方法。