JUnit怎么写?

wen python案例 6

JUnit怎么写?一篇让你彻底掌握单元测试的实战指南

目录导读

  1. JUnit是什么?为什么必须学?
  2. JUnit环境搭建:5分钟搞定第一个测试
  3. JUnit核心注解全解析
  4. 断言方法:如何验证测试结果?
  5. 参数化测试:一次编写,多组数据验证
  6. Mockito配合JUnit:解决依赖难题
  7. 常见错误与最佳实践
  8. 问答精选:解决你90%的疑惑

JUnit是什么?为什么必须学?

JUnit是Java生态中最流行的单元测试框架,它的核心思想是:用代码测试代码

  • 测试驱动开发(TDD)基石:先写测试,再写实现
  • 回归测试利器:修改代码后一键运行,立刻知道是否破坏原有功能
  • CI/CD必备:Jenkins、GitLab CI等工具自动执行测试

关键问答

:JUnit 4和JUnit 5哪个更推荐?
:推荐JUnit 5,它模块化更强,支持Java 8+特性,且完全向后兼容JUnit 4。

:单元测试只测公共方法吗?
:通常只测公共API,但若私有方法逻辑复杂,可通过反射或重构方式来测试。


JUnit环境搭建:5分钟搞定第一个测试

第一步:添加Maven依赖

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Gradle用户:

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

第二步:编写第一个测试类

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
    @Test
    void testAdd() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals(5, result, "2+3应该等于5");
    }
}

第三步:运行测试

  • IDE直接运行:IntelliJ或Eclipse中点击绿色三角
  • Maven运行mvn test
  • Gradle运行gradle test

输出示例
JUnit怎么写?


JUnit核心注解全解析

注解 作用 执行时机
@Test 标记一个测试方法 每次测试运行时
@BeforeEach 每个测试方法前执行 准备数据/对象
@AfterEach 每个测试方法后执行 清理资源
@BeforeAll 所有测试前执行一次 静态方法,初始化全局资源
@AfterAll 所有测试后执行一次 静态方法,释放全局资源
@Disabled 禁用某测试方法 不执行,显示为跳过

完整示例

class DatabaseTest {
    private static Connection conn;
    @BeforeAll
    static void setupConnection() {
        conn = DriverManager.getConnection("jdbc:h2:mem:test");
    }
    @BeforeEach
    void cleanTable() {
        conn.createStatement().execute("DELETE FROM users");
    }
    @Test
    void testInsert() {
        // 测试插入逻辑
    }
    @AfterEach
    void logTestEnd() {
        System.out.println("测试结束");
    }
    @AfterAll
    static void closeConnection() throws Exception {
        conn.close();
    }
}

断言方法:如何验证测试结果?

断言是测试的灵魂,JUnit 5提供丰富的断言方法:

常用断言

assertEquals(4, calculator.multiply(2, 2)); // 相等
assertNotEquals(5, calculator.multiply(2, 2)); // 不相等
assertTrue(result > 0); // 布尔真
assertFalse(result < 0); // 布尔假
assertNull(obj); // 对象为空
assertNotNull(obj); // 对象非空
assertThrows(IllegalArgumentException.class, () -> {
    calculator.divide(1, 0);
}); // 预期异常
assertTimeout(Duration.ofMillis(100), () -> {
    Thread.sleep(50);
}); // 超时断言

进阶:assertAll组合断言

assertAll("用户属性验证",
    () -> assertEquals("小明", user.getName()),
    () -> assertTrue(user.getAge() > 0),
    () -> assertNotNull(user.getEmail())
);
// 即使某个断言失败,其他断言仍会执行,并收集所有错误

参数化测试:一次编写,多组数据验证

当需要测试多组输入输出时,参数化测试避免重复代码。

添加依赖(JUnit 5内置)

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

常见数据来源

// 方式1:@ValueSource(简单值)
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "level"})
void testPalindrome(String word) {
    assertTrue(isPalindrome(word));
}
// 方式2:@CsvSource(表格数据)
@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "2, 3, 5",
    "10, 20, 30"
})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, calculator.add(a, b));
}
// 方式3:@MethodSource(自定义数据工厂)
@ParameterizedTest
@MethodSource("provideUsers")
void testUserValidation(User user, boolean expected) {
    assertEquals(expected, validator.isValid(user));
}
static Stream<Arguments> provideUsers() {
    return Stream.of(
        Arguments.of(new User("", 20), false),
        Arguments.of(new User("张三", 17), false),
        Arguments.of(new User("李四", 18), true)
    );
}

优点:大幅减少重复代码,测试覆盖率更高。


Mockito配合JUnit:解决依赖难题

真实项目中,类常依赖数据库、外部API等,Mockito可以模拟这些依赖。

添加Mockito依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.4.0</version>
    <scope>test</scope>
</dependency>

实战示例

import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) // JUnit 5集成
class OrderServiceTest {
    @Mock
    private PaymentGateway paymentGateway; // 模拟支付网关
    @InjectMocks
    private OrderService orderService; // 自动注入mock
    @Test
    void testCreateOrder() {
        // 设置mock行为
        when(paymentGateway.charge(anyDouble())).thenReturn(true);
        Order order = orderService.createOrder(100.0);
        // 验证支付网关被调用
        verify(paymentGateway).charge(100.0);
        assertEquals(OrderStatus.PAID, order.getStatus());
    }
    @Test
    void testPaymentFailed() {
        when(paymentGateway.charge(anyDouble())).thenReturn(false);
        assertThrows(PaymentException.class, () -> {
            orderService.createOrder(200.0);
        });
    }
}

重要技巧

  • @Mock创建模拟对象
  • @InjectMocks自动注入mock到被测类
  • when().thenReturn()定义mock行为
  • verify()验证方法是否被调用

常见错误与最佳实践

错误1:测试依赖于执行顺序

// 错误示例
@Test void test1() { /* 修改全局状态 */ }
@Test void test2() { /* 依赖test1的状态 */ }

正确做法:每个测试独立,用@BeforeEach初始化。

错误2:测试数据库依赖

// 避免
@Test
void testQuery() {
    // 直接连接生产数据库
}

正确做法:使用H2内存数据库或Mock DAO层。

错误3:断言信息写得太少

// 不推荐
assertEquals(5, result);
// 推荐
assertEquals(5, result, "计算2+3时预期为5,实际得到:" + result);

最佳实践清单

  1. 命名规范testMethodName_Scenario_ExpectedResult,如testDivide_ByZero_ThrowsException
  2. 一个测试只测一个概念:不要在一个@test中测试多个无关场景
  3. 使用TDD:先写失败测试,再写实现
  4. 保持测试快速:避免网络IO、数据库连接
  5. 代码覆盖率不是目标:重点测试核心逻辑和边界情况

问答精选:解决你90%的疑惑

问:如何测试私有方法?
:不推荐直接测试私有方法,应测试调用私有方法的公有方法,若必须测,可用PowerMock或提取为工具类(改为公有)。

问:测试中如何模拟时间?(如过期判断)
:使用java.time.Clock注入,测试时传入固定时间的Clock实例。

问:JUnit 5中如何忽略测试?
:使用@Disabled("原因说明")

问:多个测试类怎么组织?
:每个类对应一个被测类,按包结构组织,例如src/test/java/com/example/service/UserServiceTest.java

问:如何生成测试报告?
:Maven使用maven-surefire-report-plugin,Gradle使用test-report任务,CI工具(如Jenkins)会自动收集测试报告。


JUnit的核心在于:

  • 注解驱动:通过@Test@BeforeEach等管理生命周期
  • 断言验证assertEqualsassertThrows等确保结果正确
  • 参数化:用@ParameterizedTest减少重复代码
  • Mock协作:Mockito隔离外部依赖
  • 独立性:每个测试互不干扰,可重复执行

当你掌握了这些技术,编写JUnit测试不再是一种负担,而是提升代码质量和开发效率的利器,从今天开始,为你的每一个Java方法都写上一个测试吧!

最后送你一句话:没有测试的代码,不是完整的代码。

抱歉,评论功能暂时关闭!