Abhinav Pandey 2024-07-11
1. 概述
在本教程中,我们将探讨在 Spring Boot 测试中如何使用 Spring 的 @Autowired 注解和 Mockito 的 @InjectMocks 注解来注入依赖。我们会分析适用这些注解的场景,并通过示例进行说明。
2. 理解测试注解
在开始编写代码示例之前,我们先快速了解一下一些常用测试注解的基础知识。
首先,Mockito 最常用的 @Mock 注解用于为依赖项创建一个模拟(mock)实例,通常与 @InjectMocks 配合使用,后者会将标记为 @Mock 的模拟对象注入到被测试的目标对象中。
除了 Mockito 提供的注解外,Spring Boot 还提供了 @MockBean 注解,可用于创建一个模拟的 Spring Bean。该模拟 Bean 可以被 Spring 上下文中的其他 Bean 使用。此外,如果 Spring 上下文已经自动创建了某些 Bean(无需模拟),我们可以直接使用 @Autowired 注解将其注入。
3. 示例设置
在我们的代码示例中,我们将创建一个包含两个依赖的服务类,然后探索如何使用上述注解对该服务进行测试。
3.1. 依赖项
首先,添加所需的依赖项。我们将引入 Spring Boot Starter Web 和 Spring Boot Starter Test:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.3.2</version>
<scope>test</scope>
</dependency>
此外,我们还需要添加 Mockito Core 依赖,用于模拟服务:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
</dependency>
3.2. DTO
接下来,创建一个将在服务中使用的 DTO 类:
public class Book {
private String id;
private String name;
private String author;
// 构造函数、getter/setter 方法
}
3.3. 服务类
现在来看我们的服务类。首先定义一个负责数据库交互的服务:
@Service
public class DatabaseService {
public Book findById(String id) {
// 查询数据库并返回一本书
return new Book("id", "Name", "Author");
}
}
这里我们省略了具体的数据库交互逻辑,因为这与本示例无关。我们使用 @Service 注解将该类声明为 Spring 的 Service 类型 Bean。
接下来,定义一个依赖于上述服务的服务类:
@Service
public class BookService {
private DatabaseService databaseService;
private ObjectMapper objectMapper;
BookService(DatabaseService databaseService, ObjectMapper objectMapper) {
this.databaseService = databaseService;
this.objectMapper = objectMapper;
}
String getBook(String id) throws JsonProcessingException {
Book book = databaseService.findById(id);
return objectMapper.writeValueAsString(book);
}
}
这个服务类包含一个 getBook() 方法,它使用 DatabaseService 从数据库获取书籍对象,再通过 Jackson 的 ObjectMapper 将其转换为 JSON 字符串返回。
因此,该服务有两个依赖:DatabaseService 和 ObjectMapper。
4. 测试
服务类准备就绪后,我们来看看如何使用前面提到的注解对 BookService 进行测试。
4.1. 使用 @Mock 和 @InjectMocks
第一种方式是使用 @Mock 模拟服务的所有依赖,并通过 @InjectMocks 将它们注入到待测服务中。我们创建一个对应的测试类:
@ExtendWith(MockitoExtension.class)
class BookServiceMockAndInjectMocksUnitTest {
@Mock
private DatabaseService databaseService;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private BookService bookService;
@Test
void givenBookService_whenGettingBook_thenBookIsCorrect() throws JsonProcessingException {
Book book1 = new Book("1234", "Inferno", "Dan Brown");
when(databaseService.findById(eq("1234"))).thenReturn(book1);
when(objectMapper.writeValueAsString(any())).thenReturn(new ObjectMapper().writeValueAsString(book1));
String bookString1 = bookService.getBook("1234");
Assertions.assertTrue(bookString1.contains("Dan Brown"));
}
}
首先,我们在测试类上添加 @ExtendWith(MockitoExtension.class) 注解。该扩展允许我们在测试中使用 Mockito 的模拟和注入功能。
接着,我们声明 DatabaseService 和 ObjectMapper 字段,并用 @Mock 注解标记,从而创建它们的模拟对象。同时,我们在待测的 BookService 实例上使用 @InjectMocks 注解,这样 Mockito 会自动将前面声明的模拟依赖注入到该服务中。
最后,在测试方法中,我们为模拟对象定义行为,并调用 getBook() 方法进行验证。
注意:使用此方法时,必须模拟服务的所有依赖。例如,如果不模拟 ObjectMapper,在调用 writeValueAsString() 时会抛出 NullPointerException。
4.2. 使用 @Autowired 与 @MockBean
在上述方法中,我们模拟了所有依赖。但在某些情况下,我们可能只想模拟部分依赖(比如只模拟 DatabaseService),而保留其他依赖(如 ObjectMapper)的真实实现。
由于此时需要加载 Spring 上下文,我们可以结合使用 @Autowired 和 @MockBean:
@SpringBootTest
class BookServiceAutowiredAndInjectMocksUnitTest {
@MockBean
private DatabaseService databaseService;
@Autowired
private BookService bookService;
@Test
void givenBookService_whenGettingBook_thenBookIsCorrect() throws JsonProcessingException {
Book book1 = new Book("1234", "Inferno", "Dan Brown");
when(databaseService.findById(eq("1234"))).thenReturn(book1);
String bookString1 = bookService.getBook("1234");
Assertions.assertTrue(bookString1.contains("Dan Brown"));
}
}
首先,为了使用 Spring 上下文中的 Bean,我们需要在测试类上添加 @SpringBootTest 注解。接着,使用 @MockBean 标记 DatabaseService,这样 Spring 会用模拟对象替换掉原本的 DatabaseService Bean。而 BookService 则通过 @Autowired 从应用上下文中注入。
此时,BookService 中的 DatabaseService 会被替换成模拟对象,而 ObjectMapper 仍使用 Spring 容器中实际创建的 Bean。
这种方式的优势在于:无需为 ObjectMapper 模拟行为,适合在集成测试中仅对部分组件进行模拟。
4.3. 同时使用 @Autowired 和 @InjectMocks
我们也可以在上述场景中使用 @InjectMocks 而非 @MockBean。来看一下具体实现:
@Mock
private DatabaseService databaseService;
@Autowired
@InjectMocks
private BookService bookService;
@Test
void givenBookService_whenGettingBook_thenBookIsCorrect() throws JsonProcessingException {
Book book1 = new Book("1234", "Inferno", "Dan Brown");
MockitoAnnotations.openMocks(this);
when(databaseService.findById(eq("1234"))).thenReturn(book1);
String bookString1 = bookService.getBook("1234");
Assertions.assertTrue(bookString1.contains("Dan Brown"));
}
这里,我们使用 @Mock(而非 @MockBean)来模拟 DatabaseService。同时,在 BookService 上同时使用 @Autowired 和 @InjectMocks。
需要注意的是:当这两个注解一起使用时,@InjectMocks 不会自动注入模拟依赖。Spring 会先通过 @Autowired 注入真实的 BookService 实例。
但我们可以在测试中调用 MockitoAnnotations.openMocks(this) 方法,手动触发 Mockito 的注入逻辑。该方法会查找带有 @InjectMocks 的字段,并将标记为 @Mock 的对象注入进去。
我们在测试方法中、模拟行为定义之前调用该方法。这种方式适用于需要动态决定何时使用模拟对象、何时使用真实 Bean 的复杂测试场景。
5. 方法对比
现在我们已经了解了多种测试方法,下面对它们进行总结比较:
| 方法 | 描述 | 适用场景 |
|---|---|---|
@Mock + @InjectMocks |
使用 Mockito 的 @Mock 创建依赖的模拟实例,并通过 @InjectMocks 注入到被测对象中。 |
适用于单元测试,希望完全控制并模拟被测类的所有依赖。 |
@MockBean + @Autowired |
使用 Spring Boot 的 @MockBean 创建模拟的 Spring Bean,并通过 @Autowired 注入其他真实 Bean。 |
适用于集成测试,只需模拟部分 Spring Bean,其余依赖保持真实。 |
@InjectMocks + @Autowired |
使用 Mockito 的 @Mock 创建模拟对象,并通过 @InjectMocks 注入到已由 Spring 自动装配的 Bean 中。 |
提供灵活性,适用于需要临时覆盖 Spring 注入的 Bean 的复杂测试场景。 |
6. 结论
本文介绍了 Mockito 与 Spring Boot 中常用测试注解(@Mock、@InjectMocks、@Autowired 和 @MockBean)的不同使用方式。我们探讨了根据测试需求选择合适注解组合的策略:
- 纯单元测试 → 使用
@Mock+@InjectMocks - Spring 集成测试 → 使用
@MockBean+@Autowired - 动态/混合测试 → 使用
@Mock+@Autowired+@InjectMocks+MockitoAnnotations.openMocks()
合理选择这些注解,可以显著提升测试的可维护性、可读性和可靠性。