Spring Test 从入门到实战

Spring Test 从入门到实战

摘要:本文将带你从零开始,循序渐进地学习Spring Test框架。你将掌握单元测试、集成测试、Mock测试等核心技能!

Spring Test 概述

Spring Test是Spring框架提供的测试模块,它简化了Spring应用程序的测试过程。通过Spring Test,我们可以:

  • 快速编写测试:无需手动创建Spring上下文
  • 依赖注入:自动注入测试所需的Bean
  • 事务管理:支持测试中的事务回滚
  • 集成测试:轻松进行Web层、Service层的集成测试

核心优势

  1. 简化配置:通过注解自动配置测试环境
  2. 上下文缓存:提高测试执行效率
  3. 丰富的断言:提供多种断言方法
  4. Mock支持:与Mockito完美集成

环境搭建

Maven依赖配置

xml 复制代码
<dependencies>
    <!-- Spring Boot Test Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- AssertJ -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

核心注解详解

1. @SpringBootTest

java 复制代码
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void testFindUserById() {
        User user = userService.findUserById(1L);
        assertThat(user).isNotNull();
        assertThat(user.getId()).isEqualTo(1L);
    }
}

2. @TestConfiguration

java 复制代码
@TestConfiguration
class TestConfig {

    @Bean
    public UserService userService() {
        return new UserService();
    }
}

3. @AutoConfigureMockMvc

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testGetUser() throws Exception {
        mockMvc.perform(get("/api/users/1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.id").value(1));
    }
}

测试生命周期

生命周期方法

java 复制代码
@SpringBootTest
class LifecycleTest {

    @BeforeAll
    static void setup() {
        // 在所有测试方法执行前运行一次
        System.out.println("BeforeAll: 初始化测试环境");
    }

    @BeforeEach
    void beforeEach() {
        // 在每个测试方法执行前运行
        System.out.println("BeforeEach: 准备测试数据");
    }

    @Test
    void test1() {
        System.out.println("Test1: 执行测试");
    }

    @Test
    void test2() {
        System.out.println("Test2: 执行测试");
    }

    @AfterEach
    void afterEach() {
        // 在每个测试方法执行后运行
        System.out.println("AfterEach: 清理测试数据");
    }

    @AfterAll
    static void tearDown() {
        // 在所有测试方法执行后运行一次
        System.out.println("AfterAll: 清理测试环境");
    }
}

实战案例

案例1:用户服务单元测试

java 复制代码
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void testCreateUser() {
        // Given
        UserDTO userDTO = new UserDTO("张三", "zhangsan@example.com");

        // When
        User user = userService.createUser(userDTO);

        // Then
        assertThat(user).isNotNull();
        assertThat(user.getName()).isEqualTo("张三");
        assertThat(user.getEmail()).isEqualTo("zhangsan@example.com");
        assertThat(user.getId()).isNotNull();
    }

    @Test
    void testCreateUserWithExistingEmail() {
        // Given
        UserDTO userDTO = new UserDTO("李四", "zhangsan@example.com");

        // When & Then
        assertThrows(UserAlreadyExistsException.class, () -> {
            userService.createUser(userDTO);
        });
    }
}

案例2:REST API集成测试

java 复制代码
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testGetUserById() throws Exception {
        // When & Then
        mockMvc.perform(get("/api/users/1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.id").value(1))
               .andExpect(jsonPath("$.name").value("张三"));
    }

    @Test
    void testCreateUser() throws Exception {
        // Given
        String userJson = """
            {
                "name": "王五",
                "email": "wangwu@example.com"
            }
            """;

        // When & Then
        mockMvc.perform(post("/api/users")
               .contentType(MediaType.APPLICATION_JSON)
               .content(userJson))
               .andExpect(status().isCreated())
               .andExpect(jsonPath("$.name").value("王五"));
    }
}

案例3:使用Mock进行测试

java 复制代码
@SpringBootTest
class UserServiceWithMockTest {

    @MockBean
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Test
    void testFindUserById_NotFound() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.empty());

        // When & Then
        assertThrows(UserNotFoundException.class, () -> {
            userService.findUserById(1L);
        });
    }

    @Test
    void testUpdateUser_Success() {
        // Given
        User existingUser = new User(1L, "张三", "zhangsan@example.com");
        when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser));
        when(userRepository.save(any(User.class))).thenReturn(existingUser);

        UserDTO updateDTO = new UserDTO("张三三", "zhangsan3@example.com");

        // When
        User updatedUser = userService.updateUser(1L, updateDTO);

        // Then
        assertThat(updatedUser.getEmail()).isEqualTo("zhangsan3@example.com");
        verify(userRepository).save(any(User.class));
    }
}

案例4:事务测试

java 复制代码
@SpringBootTest
@Transactional
class TransactionalUserServiceTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Test
    void testCreateUser_WithTransaction() {
        // Given
        UserDTO userDTO = new UserDTO("赵六", "zhaoliu@example.com");

        // When
        User user = userService.createUser(userDTO);

        // Then
        assertThat(user).isNotNull();
        assertThat(userRepository.count()).isEqualTo(1);
    }

    @Test
    void testCreateUser_WithoutTransaction() {
        // Given
        UserDTO userDTO = new UserDTO("钱七", "qianqi@example.com");

        // When
        User user = userService.createUserWithoutTransaction(userDTO);

        // Then
        assertThat(user).isNotNull();
        // 数据会被回滚
        assertThat(userRepository.count()).isEqualTo(0);
    }
}

高级特性

1. 参数化测试

java 复制代码
@ParameterizedTest
@ValueSource(strings = {"test1@example.com", "test2@example.com", "test3@example.com"})
void testEmailValidation(String email) {
    assertThat(EmailValidator.isValidEmail(email)).isTrue();
}

@ParameterizedTest
@MethodSource("provideUserDTOs")
void testCreateUserWithDifferentData(UserDTO userDTO) {
    User user = userService.createUser(userDTO);
    assertThat(user).isNotNull();
    assertThat(user.getEmail()).isEqualTo(userDTO.getEmail());
}

private static Stream<Arguments> provideUserDTOs() {
    return Stream.of(
        Arguments.of(new UserDTO("用户1", "user1@example.com")),
        Arguments.of(new UserDTO("用户2", "user2@example.com")),
        Arguments.of(new UserDTO("用户3", "user3@example.com"))
    );
}

2. 性能测试

java 复制代码
@SpringBootTest
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PerformanceTest {

    @Autowired
    private UserService userService;

    @Test
    @Benchmark
    void testUserServicePerformance() {
        // 测试性能
        for (int i = 0; i < 1000; i++) {
            userService.findUserById((long) i);
        }
    }
}

测试覆盖率

如何提高测试覆盖率

  1. 单元测试:覆盖所有业务逻辑分支
  2. 边界测试:测试边界条件和异常情况
  3. 集成测试:验证组件间的交互
  4. 端到端测试:验证完整的业务流程

覆盖率工具

java 复制代码
// 使用JaCoCo进行覆盖率分析
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

最佳实践

1. 测试命名规范

java 复制代码
// 好的命名
@Test
void shouldCreateUserWhenValidDataProvided() {}

// 不好的命名
@Test
void test1() {}

2. 测试结构

java 复制代码
@Test
void testFeature() {
    // 1. Arrange (准备测试数据)
    UserDTO userDTO = new UserDTO("张三", "zhangsan@example.com");
    // 2. Act (执行测试操作)
    User user = userService.createUser(userDTO);
    // 3. Assert (验证结果)
    assertThat(user).isNotNull();
    assertThat(user.getName()).isEqualTo("张三");
}

3. 异步测试

java 复制代码
@Test
void testAsyncOperation() throws InterruptedException, ExecutionException {
    // Given
    CompletableFuture<User> future = userService.asyncCreateUser(
        new UserDTO("李四", "lisi@example.com"));

    // When
    User user = future.get();

    // Then
    assertThat(user).isNotNull();
    assertThat(user.getName()).isEqualTo("李四");
}

依赖关系

总结

Spring Test是Java开发中不可或缺的工具,掌握它将大大提高你的开发效率和代码质量。

相关推荐
WeiXiao_Hyy1 天前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
苏渡苇1 天前
优雅应对异常,从“try-catch堆砌”到“设计驱动”
java·后端·设计模式·学习方法·责任链模式
团子的二进制世界1 天前
G1垃圾收集器是如何工作的?
java·jvm·算法
long3161 天前
Aho-Corasick 模式搜索算法
java·数据结构·spring boot·后端·算法·排序算法
独断万古他化1 天前
【SSM开发实战:博客系统】(三)核心业务功能开发与安全加密实现
spring boot·spring·mybatis·博客系统·加密
rannn_1111 天前
【苍穹外卖|Day4】套餐页面开发(新增套餐、分页查询、删除套餐、修改套餐、起售停售)
java·spring boot·后端·学习
灵感菇_1 天前
Java HashMap全面解析
java·开发语言
qq_12498707531 天前
基于JavaWeb的大学生房屋租赁系统(源码+论文+部署+安装)
java·数据库·人工智能·spring boot·计算机视觉·毕业设计·计算机毕业设计
短剑重铸之日1 天前
《设计模式》第十一篇:总结
java·后端·设计模式·总结
若鱼19191 天前
SpringBoot4.0新特性-Observability让生产环境更易于观测
java·spring