Java单元测试JUnit5实战教程:从入门到精通,实战技巧全解析

Java单元测试JUnit5实战教程:从入门到精通,实战技巧全解析 一

文章目录CloseOpen

我之前带团队的时候,就遇到过这种情况:有个项目上线三个月,光修复测试没覆盖到的bug就花了20多个人天。后来我们复盘发现,不是大家不想写测试,而是很多人觉得单元测试“麻烦”“不知道从哪下手”,尤其对JUnit5这种新框架,看着官方文档里一堆注解就头大。今天我就把自己带团队时 的“JUnit5实战手册”分享给你,从环境搭建到高级技巧,全是能直接上手的干货,亲测带新人的时候这么教,他们一周内就能写出规范的测试用例。

JUnit5基础上手:环境搭建与核心注解实战

环境搭建:3步搞定Maven/Gradle配置

很多人觉得“搭环境”是个坎,其实JUnit5的配置比你想的简单多了。不管你用Maven还是Gradle,3步就能搞定,我带的实习生按这个步骤走,最快5分钟就能跑通第一个测试。

第一步:添加依赖

。如果你用Maven,直接在pom.xml里加这几行(记得把版本号换成最新的,现在稳定版是5.10.0):

 

<!-

  • JUnit5核心依赖 >
  • org.junit.jupiter

    junit-jupiter-api

    5.10.0

    test

    <!-

  • 测试运行器,确保IDEA或Maven能执行测试 >
  • org.junit.jupiter

    junit-jupiter-engine

    5.10.0

    test

    如果你用Gradle,就在build.gradle里加:

    dependencies { 

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'

    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'

    }

    test {

    useJUnitPlatform() // 启用JUnit5平台

    }

    我之前帮一个朋友配置Gradle的时候,他漏了最后一行useJUnitPlatform(),结果测试跑不起来,折腾了半小时,你可别犯这个错。

    第二步:创建测试类

    。在Java项目里,测试类一般放在src/test/java目录下,包名和被测试类保持一致。比如你有个com.example.service.UserService,测试类就叫UserServiceTest,这样找起来一目了然。IDEA里可以直接右键被测试类,选“Generate”→“Test”,自动生成测试类模板,特别方便。 第三步:写第一个测试方法。最简单的测试方法长这样:

    import org.junit.jupiter.api.Test; 

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

    public class CalculatorTest {

    @Test

    void add() {

    Calculator calculator = new Calculator();

    int result = calculator.add(2, 3);

    assertEquals(5, result); // 断言结果是否为5

    }

    }

    你可能会问,就这么简单?对,JUnit5的设计理念就是“简洁易用”。点一下IDEA左侧的运行按钮,测试通过会显示绿色对勾,失败会显示红色波浪线,还会告诉你预期5实际多少,特别直观。我见过有人写测试非要搞复杂的结构,其实大可不必,简单的测试才容易维护。

    核心注解解密:从@Test到@AfterAll的正确打开方式

    搞定了环境,接下来你得掌握JUnit5的核心注解——这些“标签”能帮你控制测试的执行流程。我见过不少人乱用注解,比如用@BeforeAll初始化数据库连接,结果测试互相干扰,最后还怪框架不好用。其实是你没搞懂每个注解的“脾气”。

    先看一张我整理的JUnit5常用注解对比表,帮你快速理清思路:

    注解名称 作用 使用场景 注意事项
    @Test 标记测试方法 所有需要执行的测试用例 无返回值,可抛异常
    @BeforeEach 每个测试方法前执行 初始化测试对象(如new UserService()) 非静态方法
    @AfterEach 每个测试方法后执行 清理资源(如关闭文件流) 非静态方法
    @BeforeAll 所有测试方法前执行1次 初始化全局资源(如数据库连接池) 必须是静态方法
    @AfterAll 所有测试方法后执行1次 销毁全局资源(如关闭连接池) 必须是静态方法

    表格里有个关键点:@BeforeEach@BeforeAll的区别。你可能会问,为什么需要“每个测试前执行”和“所有测试前执行”两种?举个例子:假设你测试一个用户服务,每个测试方法都需要一个新的UserService对象(避免测试互相影响),这时候用@BeforeEach;但数据库连接池只需要初始化一次,就用@BeforeAll。我之前有个同事把@BeforeAll写成了非静态方法,结果IDEA直接报错,你写的时候记得检查一下。

    再说说@Test注解,JUnit5的@Test比JUnit4强大多了——它支持直接抛异常,比如测试某个方法在参数错误时会抛异常,你可以这么写:

    @Test 

    void divide_WhenDenominatorZero_ShouldThrowException() {

    Calculator calculator = new Calculator();

    assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0));

    }

    这里的assertThrows是JUnit5自带的断言方法,不用像JUnit4那样写try-catch,简洁多了。你想想,要是用JUnit4,得写try { calculator.divide(1,0); fail(); } catch (ArithmeticException e) {},多麻烦!

    还有个注解你可能会用到:@Disabled,用来暂时禁用某个测试。比如某个功能还在开发中,测试跑不通,你可以标上@Disabled("功能开发中,暂不测试"),这样构建的时候就会跳过它,等功能好了再打开。我之前做迭代开发的时候,经常用这个注解,避免测试失败影响整个构建流程。

    JUnit5高级技巧:从参数化测试到Mock框架集成

    学会了基础,你可能会觉得“写单个测试还行,但重复代码好多”——比如测试一个加法方法,要测正数+正数、正数+负数、零+零,难道要写三个测试方法?这时候就需要“参数化测试”来拯救你了。再往深了说,当你的代码依赖数据库、Redis这些外部服务时,怎么测试才能不连真实环境?这就需要Mock框架了。这部分我会带你一步步搞定这些“进阶难题”。

    参数化测试:让一个测试用例跑遍所有场景

    我见过最夸张的测试代码,是一个校验手机号格式的方法,写了10个测试方法,分别测试11位数字、少于11位、多于11位、有字母的情况……代码重复得不行,改一点逻辑就要改10个地方。其实用JUnit5的参数化测试,一行代码能顶10行用。

    参数化测试的核心思想是:用不同的参数多次运行同一个测试方法。要想用起来,你需要先加个依赖(Maven):

     

    org.junit.jupiter

    junit-jupiter-params

    5.10.0

    test

    然后用@ParameterizedTest注解代替@Test,再指定参数来源。最常用的参数来源有四种,我给你一一举例:

    第一种:@ValueSource

    ——适合简单的单参数场景。比如测试手机号长度:

    @ParameterizedTest 

    @ValueSource(strings = {"13800138000", "13912345678"}) // 正确的手机号

    void validatePhone_ValidNumbers_ShouldReturnTrue(String phone) {

    PhoneValidator validator = new PhoneValidator();

    assertTrue(validator.validate(phone));

    }

    @ParameterizedTest

    @ValueSource(strings = {"12345", "138001380000", "abc123456789"}) // 错误的手机号

    void validatePhone_InvalidNumbers_ShouldReturnFalse(String phone) {

    PhoneValidator validator = new PhoneValidator();

    assertFalse(validator.validate(phone));

    }

    这样一个测试方法,会分别用@ValueSource里的每个参数跑一遍,IDEA的测试结果里会显示“[1] 13800138000”“[2] 13912345678”,清晰得很。

    第二种:@CsvSource

    ——适合多参数场景。比如测试加法,要传两个参数(a和b)和预期结果:

    @ParameterizedTest 

    @CsvSource({

    "2, 3, 5", // 2+3=5

    "-1, 1, 0", // -1+1=0

    "0, 0, 0" // 0+0=0

    })

    void add(int a, int b, int expected) {

    Calculator calculator = new Calculator();

    assertEquals(expected, calculator.add(a, b));

    }

    这里的@CsvSource用逗号分隔参数,一行就是一组测试数据。你看,原来要写三个测试方法,现在一个就够了,维护起来多方便!

    第三种:@MethodSource

    ——适合复杂参数生成。如果你的测试数据需要通过代码生成(比如从文件读数据),就用这个。先写个静态方法返回Stream

    private static Stream provideDivisionTestCases() { 

    return Stream.of(

    Arguments.of(6, 2, 3), // 6/2=3

    Arguments.of(5, -1, -5), // 5/-1=-5

    Arguments.of(0, 5, 0) // 0/5=0

    );

    }

    @ParameterizedTest

    @MethodSource("provideDivisionTestCases")

    void divide(int dividend, int divisor, int expected) {

    Calculator calculator = new Calculator();

    assertEquals(expected, calculator.divide(dividend, divisor));

    }

    这种方式灵活性最高,我之前测试一个订单金额计算方法,需要从Excel读100组测试数据,就是用@MethodSource调用POI读取Excel,特别方便。

    可能你会担心:参数化测试跑这么多遍,会不会很慢?其实单元测试本来就应该快,如果你觉得慢,可能是测试里做了太多IO操作(比如连数据库),这时候就需要Mock框架来解决了。

    Mockito集成:解决外部依赖的测试难题

    “我写的代码要调数据库,不连数据库怎么测试?”这是我被问得最多的问题。答案是:用Mock框架“假装”一个数据库。现在最流行的Mock框架是Mockito,和JUnit5配合得天衣无缝。

    先说说为什么需要Mock。比如你有个UserService,它依赖UserRepository(数据库访问层):

    @Service 

    public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {

    this.userRepository = userRepository;

    }

    public User getUserById(Long id) {

    return userRepository.findById(id)

    .orElseThrow(() -> new UserNotFoundException("用户不存在"));

    }

    }

    测试getUserById时,如果连真实数据库,万一数据库里没数据、或者数据被改了,测试就会失败。这时候我们可以用Mockito“Mock”一个UserRepository,让它按我们的想法返回数据。

    第一步:添加依赖

    (Maven):

     

    org.mockito

    mockito-core

    4.11.0

    test

    org.mockito

    mockito-junit-jupiter

    4.11.0

    test

    第二步:用@Mock和@InjectMocks创建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 java.util.Optional;

    import static org.mockito.Mockito.when;

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

    @ExtendWith(MockitoExtension.class) // 启用Mockito扩展

    class UserServiceTest {

    @Mock // Mock一个UserRepository

    private UserRepository userRepository;

    @InjectMocks // 把Mock的userRepository注入到UserService

    private UserService userService;

    @Test

    void getUserById_WhenUserExists_ShouldReturnUser() {

    //

  • 准备测试数据
  • Long userId = 1L;

    User mockUser = new User(userId, "张三");

    //

  • 告诉Mock对象:当调用findById(1L)时,返回mockUser
  • when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

    //

  • 调用被测试方法
  • User result = userService.getUserById(userId);

    //

  • 断言结果
  • assertEquals("张三", result.getName());

    }

    @Test

    void getUserById_WhenUserNotExists_ShouldThrowException() {

    // 告诉Mock对象:当调用findById(999L)时,返回空Optional

    when(userRepository.findById(999L)).thenReturn(Optional.empty());

    // 断言会抛出UserNotFoundException

    assertThrows(UserNotFoundException.class, () -> userService.getUserById(999L));

    }

    }

    这里的关键是when(...).thenReturn(...)——你可以理解为“给Mock对象写剧本”:当它收到某个调用时,就返回你指定的数据。这样测试就完全不依赖真实数据库了,跑起来飞快,而且结果稳定。

    我之前带团队推广Mockito的时候,有个同事问:“怎么确定Mock的逻辑对不对?万一我Mock错了,测试通过但实际代码有问题怎么办?”这就要说到“验证Mock调用”了。比如你想确认userRepository.findById被调用了一次,可以用verify

    java

    import static org.mockito.Mockito.verify;

    @Test

    void getUserById_ShouldCallRepositoryOnce() {

    Long userId = 1L;

    when(userRepository.findById(userId)).thenReturn(Optional.of(new User(userId, “张三”)));

    userService.getUserById


    你用JUnit4的时候是不是觉得框架有点“死板”?JUnit5最大的变化就是把自己拆成了好几个灵活的部分,就像搭积木一样各司其职。它主要分三块:JUnit Platform是“舞台”,负责运行测试,不管是JUnit5自己的测试,还是老的JUnit3/4测试,甚至其他框架的测试,都能在这上面跑;JUnit Jupiter是“演员”,提供了新的编程模型,像那些@ParameterizedTest注解、Lambda表达式支持,都是它的活儿;还有个JUnit Vintage,专门用来“兼容老演员”,如果你项目里还有没来得及升级的JUnit4测试用例,靠它就能继续跑,不用一下子全改完。我之前带团队从JUnit4升到JUnit5的时候,就是先用Vintage过渡,把老测试保住,再慢慢用Jupiter写新测试,特别省心。

    除了架构灵活,JUnit5对Java 8的支持也让代码清爽了不少。你想想,JUnit4里写个断言抛异常,得用try-catch包起来,还得手动fail(),现在用Jupiter的assertThrows,直接传个Lambda表达式就行,比如assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0)),一行搞定。注解也换了新的,JUnit4的@Before现在叫@BeforeEach,@After叫@AfterEach,名字更直白——“每个测试前”“每个测试后”,一看就知道啥意思。最舒服的是测试方法终于不用写public了,以前写@Test public void test() {},现在直接@Test void test() {},少敲好几个字母,日积月累能省不少事儿。我见过有人升级后还习惯性写public,结果IDEA直接标黄提醒“这没必要啦”,特别有意思。

    功能上也加了不少“实用工具”,比如原生支持参数化测试。JUnit4的时候想跑多组数据,得装第三方插件,现在@ValueSource、@CsvSource这些注解直接能用,像测试手机号校验,以前要写五六个@Test方法,现在一个@ParameterizedTest加@CsvSource就能把各种情况列全,代码量少了一半还多。动态测试也很赞,能在运行时动态生成测试用例,比如从文件里读测试数据,读多少条就生成多少个测试,特别适合数据驱动的场景。这些功能加起来,让写测试从“不得不做的任务”变成了“还挺顺手的事儿”,至少我团队里的人用了半年后,再也没人抱怨写测试麻烦了。


    JUnit5和JUnit4有什么主要区别?

    JUnit5相比JUnit4的核心区别在于架构模块化和功能增强:JUnit5由JUnit Platform(平台)、JUnit Jupiter(编程模型)和JUnit Vintage(兼容JUnit3/4)三部分组成,支持Java 8及以上特性(如Lambda表达式、Stream API);注解方面,JUnit5使用@BeforeEach/@AfterEach替代JUnit4的@Before/@After,@BeforeAll/@AfterAll需配合static关键字,且测试方法不再要求public修饰; JUnit5原生支持参数化测试、动态测试,断言方法更丰富(如assertThrows、assertTimeout),整体更灵活易用。

    单元测试覆盖率达到多少才算合格?

    单元测试覆盖率没有绝对统一的标准,需结合项目类型和团队规范。一般 核心业务逻辑覆盖率不低于80%,工具类、通用组件可适当放宽至60%-70%。但需注意“为覆盖率而测试”的误区——覆盖率高不代表测试质量好,关键是覆盖边界场景(如异常处理、空值校验)和核心逻辑分支。可通过JaCoCo等工具生成覆盖率报告,重点关注未覆盖的代码块是否存在风险。

    参数化测试适合哪些场景?

    参数化测试适合需要多组输入验证同一逻辑的场景,例如:

  • 边界值测试(如数值范围、字符串长度);
  • 多条件校验(如手机号格式、密码复杂度规则);3. 异常场景覆盖(如空值、非法参数处理)。通过@ValueSource、@CsvSource等注解,可减少重复代码,让测试用例更清晰,尤其适合工具类、校验逻辑等输入输出关系明确的模块。
  • Mockito只能Mock接口吗?可以Mock具体类吗?

    Mockito不仅可以Mock接口,也支持Mock具体类(需满足条件)。对于非final的具体类,可直接通过Mockito.mock(Class)方法创建Mock对象;但final类、static方法、final方法默认无法Mock,需通过Mockito的inline扩展(添加mockito-inline依赖)或PowerMock等工具处理。实际开发中, 优先依赖接口设计(如Service依赖Repository接口),便于Mock和降低耦合。

    编写单元测试时,如何处理静态方法的依赖?

    静态方法因耦合性高,测试时较难Mock, 优先通过重构减少静态方法依赖(如将静态工具类改为实例类,通过构造函数注入)。若必须处理,可使用工具方案:

  • Mockito 3.4.0+支持通过mockito-inline依赖Mock静态方法(需配合Mockito.mockStatic(Class));
  • 借助PowerMock框架(需注意与JUnit5的兼容性);3. 对简单静态逻辑,可直接调用(如Math.abs()等无副作用的工具方法),避免过度Mock。
  • 0
    显示验证码
    没有账号?注册  忘记密码?