Java 单元测试详解:从入门到实战,彻底掌握 JUnit 5 + Mockito + Spring Boot 测试技巧

发布于:2025-07-26 ⋅ 阅读:(11) ⋅ 点赞:(0)

作为一名 Java 开发工程师,你一定知道在软件开发中,代码的可维护性、可扩展性和质量保障是项目成功的关键。而单元测试(Unit Testing) 正是确保代码质量、提升开发效率、减少 Bug 的核心手段之一。

本文将带你全面掌握:

  • 什么是单元测试?
  • 为什么需要单元测试?
  • Java 常用的单元测试框架(JUnit 5、TestNG、Mockito)
  • 如何为 Java 类、Spring Boot 项目编写单元测试
  • 使用断言、Mock、Spy、参数化测试等高级技巧
  • 最佳实践与常见误区

并通过丰富的代码示例和真实项目场景讲解,帮助你写出更规范、更高效、更易维护的 Java 单元测试代码。


🧱 一、什么是单元测试?

✅ 单元测试(Unit Testing)定义:

单元测试是针对**最小可测试单元(通常是方法)**进行正确性验证的测试,通常由开发者编写,用于验证某个类或方法在特定输入下是否返回预期结果。

✅ 单元测试的特点:

特点 描述
自动化 无需人工执行,可自动运行
快速执行 单个测试用例执行时间极短
独立运行 不依赖外部系统(如数据库、网络)
可重复执行 每次运行结果一致
可集成CI/CD 与 Jenkins、GitLab CI、GitHub Actions 等集成
提高代码质量 减少 Bug、提升重构信心

🔍 二、Java 常见的单元测试框架对比

框架 特点
JUnit 5 最主流的 Java 单元测试框架,支持 Java 8+,模块化设计
TestNG 支持数据驱动、依赖测试、并行测试,适合复杂测试场景
Mockito 用于模拟对象(Mock)、验证行为(Verify)
PowerMock 可 Mock 静态方法、构造函数等(已逐渐被替代)
AssertJ 提供更流畅的断言语法,增强可读性
Spring Boot Test 集成 JUnit + Mockito,支持 Spring 上下文加载

📌 推荐组合:JUnit 5 + Mockito + Spring Boot Test,适用于大多数 Java Web 项目。


🧠 三、JUnit 5 核心概念与注解

✅ 常用注解:

注解 说明
@Test 表示一个测试方法
@BeforeEach 每个测试方法执行前执行
@AfterEach 每个测试方法执行后执行
@BeforeAll 所有测试方法执行前执行一次(静态方法)
@AfterAll 所有测试方法执行后执行一次(静态方法)
@DisplayName 设置测试类或方法的显示名称
@ParameterizedTest 参数化测试
@RepeatedTest 重复执行测试方法

🧪 四、JUnit 5 实战示例

示例1:简单单元测试

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    @DisplayName("两个整数相加应返回正确结果")
    void add_twoNumbers_returnsSum() {
        int result = calculator.add(2, 3);
        assertEquals(5, result, "2+3 应该等于5");
    }

    @Test
    void divide_byZero_throwsException() {
        Exception exception = assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        });
        assertEquals("/ by zero", exception.getMessage());
    }
}

示例2:参数化测试(@ParameterizedTest

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "5, 5, 10",
        "0, 0, 0"
    })
    void add_withDifferentInputs_returnsCorrectResult(int a, int b, int expected) {
        Calculator calculator = new Calculator();
        assertEquals(expected, calculator.add(a, b));
    }
}

🧩 五、使用 Mockito 模拟对象(Mock)

✅ 什么是 Mock?

Mock 是指模拟对象的行为,在测试中避免依赖外部系统(如数据库、网络、第三方服务),从而实现快速、独立、可重复的测试

示例:Mock 一个外部服务

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class OrderServiceTest {

    @Test
    void placeOrder_callsPaymentServiceOnce() {
        PaymentService mockPayment = mock(PaymentService.class);
        OrderService orderService = new OrderService(mockPayment);

        orderService.placeOrder(100.0);

        verify(mockPayment, times(1)).charge(100.0);
    }
}

🧪 六、Spring Boot 单元测试实战

示例:Spring Boot Controller 单元测试(MockMvc)

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

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

示例:Service 层单元测试(注入 Mock)

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)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void getUserById_returnsUserFromRepository() {
        when(userRepository.findById(1L)).thenReturn(new User("Tom"));

        User user = userService.getUserById(1L);

        assertNotNull(user);
        assertEquals("Tom", user.getName());
        verify(userRepository, times(1)).findById(1L);
    }
}

🧱 七、单元测试最佳实践

实践 描述
一个测试方法只测一个功能 保证测试单一职责
测试命名清晰、有语义 如 shouldReturnTrue_whenInputIsEven
使用断言库(JUnit、AssertJ) 提高可读性
使用 Mock 避免外部依赖 提高测试速度与稳定性
覆盖核心逻辑和边界条件 包括 null、异常、边界值等
使用参数化测试减少重复代码 提高测试覆盖率
在 CI/CD 中集成测试 确保每次提交都运行测试
使用覆盖率工具(Jacoco) 查看测试覆盖率
测试前准备、测试后清理 使用 @BeforeEach@AfterEach
使用 @SpringBootTest 进行集成测试 验证完整流程

🚫 八、常见误区与注意事项

误区 正确做法
单元测试依赖数据库 应使用 Mock 或内存数据库
测试方法不命名规范 应使用 shouldXXX_whenXXX 命名
不断言直接 return 必须使用 assertEqualsassertTrue 等断言
测试类没有注解 应使用 @ExtendWith 或 @SpringBootTest
忽略异常测试 应使用 assertThrows 测试异常
一个测试方法测多个功能 应拆分为多个测试方法
不使用参数化测试 导致重复代码
不使用断言库 代码可读性差
不在 CI 中运行测试 容易漏测
不使用覆盖率工具 无法评估测试质量

📊 九、总结:Java 单元测试核心知识点一览表

内容 说明
单元测试定义 针对最小单元(方法)进行测试
主流框架 JUnit 5、Mockito、Spring Boot Test
断言机制 assertEqualsassertTrueassertThrows
Mock 技术 使用 Mockito 模拟对象
参数化测试 @ParameterizedTest
Spring Boot 测试 @WebMvcTest@DataJpaTest@SpringBootTest
最佳实践 命名规范、单一职责、Mock 依赖、CI 集成
注意事项 不依赖外部系统、使用断言、测试覆盖率

📎 十、附录:Java 单元测试常用技巧速查表

技巧 示例
初始化测试类 @ExtendWith(MockitoExtension.class)
模拟对象 @Mock@InjectMocks
断言相等 assertEquals(expected, actual)
断言异常 assertThrows(Exception.class, () -> method())
参数化测试 @ParameterizedTest + @CsvSource
验证调用次数 verify(mock, times(1)).method()
Spring Boot 控制器测试 @WebMvcTest + MockMvc
数据层测试 @DataJpaTest
集成测试 @SpringBootTest
测试覆盖率 使用 Jacoco 或 IntelliJ 内置工具

欢迎点赞、收藏、转发,也欢迎留言交流你在实际项目中遇到的单元测试相关问题。我们下期再见 👋

📌 关注我,获取更多Java核心技术深度解析!


网站公告

今日签到

点亮在社区的每一天
去签到