> 技术文档 > Spring Boot 单元测试_springboot 单元测试

Spring Boot 单元测试_springboot 单元测试

在软件开发过程中,单元测试是确保代码质量和稳定性的重要环节。对于使用Spring Boot框架构建的应用程序,编写单元测试同样重要。本文将介绍如何在Spring Boot中编写单元测试,帮助你更好地进行软件开发和维护。

1. 为什么需要单元测试?

单元测试的主要目的是验证代码的最小可测试部分是否按预期工作。这不仅有助于发现和修复错误,还能提高代码的可维护性和可读性。对于Spring Boot应用,单元测试可以帮助你快速验证业务逻辑,避免在集成测试和生产环境中发现错误。

2. 准备工作

在Spring Boot项目中,单元测试通常使用JUnit和Mockito这两个框架。JUnit是Java平台的单元测试框架,而Mockito则是一个模拟对象框架,用于创建和配置模拟对象。

  • JUnit:Spring Boot默认使用JUnit 5作为单元测试框架。
  • Mockito:用于模拟依赖对象,方便隔离测试。

Spring Boot 提供了一个名为 spring-boot-starter-test 的启动器依赖,其中包含了常用的测试框架和工具,如 JUnit、Mockito、Spring Test、Hamcrest 等。你需要在pom.xml文件中添加以下依赖:

  org.springframework.boot spring-boot-starter-test test 

3. 单元测试入门

假设你有一个简单的服务类UserService,我们来编写一个单元测试来验证它。

@Servicepublic class UserService { public String getUserById(String id) { if (\"1\".equals(id)) { return \"User One\"; } else { return \"Unknown User\"; } }}

对于UserService,我们可以编写一个单元测试类UserServiceTest,在Idea中创建测试类的方式有二:

  • 点击测试类,Ctrl + Shift + T
  • 鼠标右键点击类名 使用 goto-Test 即可实现
import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.assertEquals;@SpringBootTestpublic class UserServiceTest { @Autowired private UserService userService; @BeforeAll static void setupBeforeAll() { // 在所有测试方法运行之前执行一次 System.out.println(\"Setup before all tests\"); } @BeforeEach void setupBeforeEach() { // 在每个测试方法运行之前执行 System.out.println(\"Setup before each test\"); } @Test public void testGetUserById() { String userId = \"1\"; String expected = \"User One\"; String actual = userService.getUserById(userId); assertEquals(expected, actual, \"User should be User One\"); } @AfterEach void teardownAfterEach() { // 在每个测试方法运行之后执行 System.out.println(\"Teardown after each test\"); } @AfterAll static void teardownAfterAll() { // 在所有测试方法运行之后执行一次 System.out.println(\"Teardown after all tests\"); }}

在这个测试中,我们使用了@SpringBootTest注解来加载完整的Spring应用上下文,然后通过@Autowired注解注入了UserService。最后,我们使用@Test注解定义了一个测试方法testGetUserById,用来验证UserServicegetUserById方法。

功能测试过程中的几个关键要素及支撑方式如下:

  • 测试运行环境:通过@SpringBootTest启动spring容器。
  • mock能力:Mockito提供了强大mock功能。
  • 断言能力:AssertJ、Hamcrest、JsonPath提供了强大的断言能力。

3.1. @SpringBootTest注解

@SpringBootTest注解的主要作用是告诉Spring Boot测试框架,当前测试类需要加载完整的Spring应用上下文。这意味着所有的配置类、Bean以及相关的组件都会被加载和初始化,以便测试能够在一个与实际运行环境相似的环境中进行。@SpringBootTest注解有一些属性可以配置,以满足不同的测试需求:

  • classes:指定需要加载的具体配置类或启动类。如果不指定,则默认加载主配置类。
  • webEnvironment:指定测试的Web环境模式。可选值包括:
    • MOCK:使用Mock的Servlet环境,不提供真实的Servlet容器。这是默认值。
    • RANDOM_PORT:使用真实的Servlet容器,并随机分配一个端口。
    • DEFINED_PORT:使用真实的Servlet容器,并使用在application.propertiesapplication.yml中定义的端口。
    • NONE:不提供Servlet容器。

3.2. 测试生命周期

JUnit 5 提供了多种注解来控制测试的生命周期,包括 @BeforeAll, @BeforeEach, @AfterEach, 和 @AfterAll。这些注解可用于设置和清理测试环境。

  • @BeforeAll:在所有测试方法运行之前执行一次。这个方法必须是静态的。
  • @BeforeEach:在每个测试方法运行之前执行。
  • @Test:用于标记一个测试方法。
  • @AfterEach:在每个测试方法运行之后执行。
  • @AfterAll:在所有测试方法运行之后执行一次。这个方法必须是静态的。

3.3. 使用断言进行验证

断言是测试中用于验证代码行为的关键部分。JUnit提供了丰富的断言方法来帮助你验证各种条件。以下是一些常用的断言方法:

  • assertEquals(expected, actual, message):验证两个值是否相等。
  • assertNotEquals(expected, actual, message):验证两个值是否不相等。
  • assertTrue(condition, message):验证条件是否为真。
  • assertFalse(condition, message):验证条件是否为假。
  • assertNull(actual, message):验证对象是否为null。
  • assertNotNull(actual, message):验证对象是否不为null。
  • assertSame(expected, actual, message):验证两个对象是否是同一个实例。
  • assertNotSame(expected, actual, message):验证两个对象是否不是同一个实例。
  • assertThrows(expectedType, executable, message):验证是否抛出了预期的异常。
  • assertDoesNotThrow(executable, message):验证是否没有抛出异常。
  • assertTimeout(duration, executable, message):验证执行时间是否在预期时间内。

举个栗子:

import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.mock.mockito.MockBean;import java.time.Duration;import static org.mockito.Mockito.*;import static org.junit.jupiter.api.Assertions.*;@SpringBootTestpublic class UserServiceTest { @MockBean private UserRepository userRepository; @Autowired private UserService userService; @Test public void testGetUserById() { // 准备测试数据 String userId = \"1\"; String expectedUserName = \"User One\"; User mockUser = new User(userId, expectedUserName); when(userRepository.findById(userId)).thenReturn(java.util.Optional.of(mockUser)); // 调用被测试的方法 String actualUserName = userService.getUserById(userId); // 使用断言进行验证 assertEquals(expectedUserName, actualUserName, \"User name should be User One\"); assertNotNull(actualUserName, \"User name should not be null\"); assertSame(mockUser.getName(), actualUserName, \"User name should be the same instance\"); } @Test public void testGetUnknownUserById() { // 准备测试数据 String userId = \"2\"; String expectedUserName = \"Unknown User\"; when(userRepository.findById(userId)).thenReturn(java.util.Optional.empty()); // 调用被测试的方法 String actualUserName = userService.getUserById(userId); // 使用断言进行验证 assertEquals(expectedUserName, actualUserName, \"User name should be Unknown User\"); assertNotNull(actualUserName, \"User name should not be null\"); } @Test public void testExceptionHandling() { // 准备测试数据 String userId = \"3\"; when(userRepository.findById(userId)).thenThrow(new RuntimeException(\"User not found\")); // 使用断言验证异常 assertThrows(RuntimeException.class, () -> { userService.getUserById(userId); }, \"Should throw RuntimeException\"); } @Test public void testTimeout() { // 准备测试数据 String userId = \"1\"; // 使用断言验证执行时间 assertTimeout(Duration.ofMillis(100), () -> { userService.getUserById(userId); }, \"Method should execute in less than 100 milliseconds\"); }}

4. 使用Mockito进行模拟测试

4.1. 模拟测试

在一些复杂场景中,你可能需要模拟某些依赖项以隔离测试。Mockito可以帮助你轻松实现这一目标。下面是一个简单的例子,使用Mockito注解来编写模拟测试类,以验证UserService的行为:

假设你有一个简单的服务类UserService,它依赖于一个UserRepository。我们将使用Mockito来模拟UserRepository,以便在测试中隔离UserService

// UserRepository.java@Repositorypublic interface UserRepository extends JpaRepository { Optional findById(String id);}// UserService.java@Servicepublic class UserService { @Autowired private UserRepository userRepository; public String getUserById(String id) { User user = userRepository.findById(id).orElse(null); return user != null ? user.getName() : \"Unknown User\"; }}

编写测试类:

import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.junit.jupiter.MockitoExtension;import static org.mockito.Mockito.*;import static org.junit.jupiter.api.Assertions.*;@ExtendWith(MockitoExtension.class)public class UserServiceTest { @Mock private UserRepository userRepository; // 模拟UserRepository @InjectMocks private UserService userService; // 需要测试的UserService @Test public void testGetUserById() { // 准备测试数据 String userId = \"1\"; String expectedUserName = \"User One\"; User mockUser = new User(userId, expectedUserName); when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); // 调用被测试的方法 String actualUserName = userService.getUserById(userId); // 验证结果 assertEquals(expectedUserName, actualUserName, \"User name should be User One\"); // 验证UserRepository的findById方法是否被调用 verify(userRepository, times(1)).findById(userId); } @Test public void testGetUnknownUserById() { // 准备测试数据 String userId = \"2\"; when(userRepository.findById(userId)).thenReturn(Optional.empty()); // 调用被测试的方法 String actualUserName = userService.getUserById(userId); // 验证结果 assertEquals(\"Unknown User\", actualUserName, \"User name should be Unknown User\"); // 验证UserRepository的findById方法是否被调用 verify(userRepository, times(1)).findById(userId); }}

在这个示例中,我们使用了以下Mockito注解:

  • @ExtendWith(MockitoExtension.class):使用Mockito的JUnit 5扩展。
  • @Mock:用于创建UserRepository的模拟对象。
  • @InjectMocks:用于创建UserService的实例,并将模拟的UserRepository注入到UserService中。

4.2. 与参数化结合使用

Mockito也可以与参数化测试结合使用,以测试不同的输入值。

import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.Arguments;import org.junit.jupiter.params.provider.MethodSource;import org.mockito.InjectMocks;import org.mockito.Mock;import java.util.Optional;import java.util.stream.Stream;import static org.mockito.Mockito.when;import static org.junit.jupiter.api.Assertions.assertEquals;public class UserServiceParameterizedTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @ParameterizedTest @MethodSource(\"provideUserIdsAndNames\") public void testGetUserById(String userId, String expectedUserName) { // 配置模拟对象的行为 when(userRepository.findById(userId)).thenReturn(Optional.of(new User(userId, expectedUserName))); // 调用被测试的方法 String actualUserName = userService.getUserById(userId); // 验证结果 assertEquals(expectedUserName, actualUserName, \"User name should match expected name\"); // 验证UserRepository的findById方法是否被调用 verify(userRepository, times(1)).findById(userId); } private static Stream provideUserIdsAndNames() { return Stream.of( Arguments.of(\"1\", \"User One\"), Arguments.of(\"2\", \"User Two\"), Arguments.of(\"3\", \"User Three\") ); }}

5. 使用Spring Boot提供的测试支持

除了上述基本用法外,Spring Boot还提供了一些额外的测试支持,比如@WebMvcTest@DataJpaTest等注解,分别用于测试Web层和数据访问层,帮助你更高效地编写单元测试。

6. 总结

单元测试是软件开发中不可或缺的一部分,它能帮助你快速发现和修复错误,确保代码的质量。Spring Boot框架提供了丰富的测试支持,帮助你轻松编写高效的单元测试。希望本文能对你有所帮助。