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开发中不可或缺的工具,掌握它将大大提高你的开发效率和代码质量。

相关推荐
roman_日积跬步-终至千里2 小时前
【SQL】SQL 语句的解析顺序:理解查询执行的逻辑
java·数据库·sql
TeamDev2 小时前
JxBrowser 8.16.0 版本发布啦!
java·chromium·浏览器自动化·jxbrowser·浏览器控件·枚举清理·跨配置文件复制密码
毕设源码-钟学长2 小时前
【开题答辩全过程】以 高校体育赛事管理系统的设计与实现为例,包含答辩的问题和答案
java
晨非辰2 小时前
C++波澜壮阔40年|类和对象篇:拷贝构造与赋值重载的演进与实现
运维·开发语言·c++·人工智能·后端·python·深度学习
Remember_9932 小时前
【LeetCode精选算法】双指针专题一
java·数据结构·算法·leetcode
未来龙皇小蓝2 小时前
策略模式:Spring Bean策略与枚举 Lambda策略
java·windows·spring boot·spring·策略模式
LiRuiJie2 小时前
从OS层面深入剖析JVM如何实现多线程与同步互斥
java·jvm·os·底层
m0_719084112 小时前
滴滴滴滴滴
java·开发语言
张乔242 小时前
spring boot项目中设置默认的方法实现
java·数据库·spring boot