HelloWorld JUnit 测试教程
HelloWorld 的 JUnit 测试其实很简单:搭建好测试框架后,写一个测试方法去断言程序输出符合预期,运行并修正失败的断言;逐步引入生命周期管理、参数化和异常断言来覆盖更多场景,最后把测试集集成到构建和 CI 流程中,保证反复可运行与易维护。

为什么要从 HelloWorld 开始学 JUnit
就像学吉他先弹一个和弦,学单元测试先写一个 HelloWorld 用例有三重好处:快速验证工具链是否可用、理解测试结构(Arrange-Act-Assert)、建立信心。别小看这个最小可运行示例,它能暴露构建配置、依赖、IDE/插件问题。
先说准备工作:环境与依赖
JUnit 有多个版本,主流是 JUnit 5(也称为 JUnit Jupiter)——功能更现代、扩展性更好。下面说明常见的两种项目类型的配置方法。
Maven(pom.xml)
在 pom.xml 添加 JUnit 5 平台和 Jupiter 引擎依赖:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
Gradle(build.gradle)
Gradle Kotlin/DSL 或 Groovy 都类似,关键是使用 junit-jupiter 平台:
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'
test {
useJUnitPlatform()
}
IDE 设置
- IntelliJ IDEA、Eclipse 都支持 JUnit 5,确保插件或 IDE 版本支持 JUnit Platform。
- 如果测试不能被识别,先运行构建工具(mvn test 或 gradle test)确认是 IDE 问题还是依赖问题。
HelloWorld 示例:一步步写一个最小可运行测试
目标:编写一个 HelloWorld 类和一个测试类,验证 hello() 方法返回字符串 “Hello, World!”(假设英语场景)。
生产代码:HelloWorld.java
public class HelloWorld {
public String hello() {
return "Hello, World!";
}
}
测试代码:HelloWorldTest.java(JUnit 5)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class HelloWorldTest {
@Test
void helloReturnsExpectedString() {
HelloWorld hw = new HelloWorld();
String actual = hw.hello();
assertEquals("Hello, World!", actual);
}
}
运行测试,如果通过你就成功了;如果失败,检查断言信息、构建输出和类路径。
核心概念:Arrange-Act-Assert(A-A-A)与测试粒度
把测试分成三步有助于可读性和排错:
- Arrange:准备测试对象与输入(构造器、模拟、初始化)。
- Act:执行被测试的方法。
- Assert:断言输出或状态符合预期。
测试粒度:一个测试最好只验证一个行为(一个断言集合),多行为测试会导致定位失败原因困难。
JUnit 5 常用注解与生命周期
JUnit 5 提供更灵活的生命周期控制,理解这些注解能让测试更高效、启动更快。
| 注解 | 用途 |
| @BeforeAll | 在所有测试之前运行一次(static 方法或在测试实例生命周期为 per-class 时可为非 static)。 |
| @AfterAll | 所有测试后运行一次,释放全局资源。 |
| @BeforeEach | 每个测试前运行(初始化状态)。 |
| @AfterEach | 每个测试后运行(清理)。 |
| @Test | 标记测试方法。 |
| @Disabled | 跳过测试(临时禁用)。 |
| @Nested | 组织嵌套测试类,便于按上下文分组测试。 |
示例:使用生命周期注解
import org.junit.jupiter.api.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class LifeCycleTest {
@BeforeAll
void setupAll() {
System.out.println("Before all tests");
}
@BeforeEach
void setup() {
System.out.println("Before each test");
}
@Test
void testOne() { /* ... */ }
@AfterEach
void tearDown() { /* ... */ }
@AfterAll
void tearDownAll() { /* ... */ }
}
注意:默认情况下 JUnit 每个测试方法会创建新的测试实例(per-method),如果要在 @BeforeAll 使用非静态方法可以切换为 per-class。
断言详解:常用 Assertions 与使用建议
断言是测试的核心,JUnit 提供了丰富的方法。用对断言能让失败的信息更清晰。
| 断言 | 用途 |
| assertEquals(expected, actual) | 值相等 |
| assertTrue(condition) | 布尔为真 |
| assertNull(obj) | 对象为 null |
| assertThrows(Exception.class, () -> …) | 断言抛出指定异常 |
| assertAll(…) | 分组断言,保证多个断言都执行并汇总失败信息 |
示例:assertAll 用法
import static org.junit.jupiter.api.Assertions.*;
@Test
void multiAssertions() {
Person p = new Person("Alice", 30);
assertAll("person",
() -> assertEquals("Alice", p.getName()),
() -> assertEquals(30, p.getAge())
);
}
*提示:* 当你需要同时验证多个互不依赖的属性时用 assertAll,这样一个断言失败不会短路掉其它断言。
参数化测试:覆盖多组输入
参数化测试可以用少量代码覆盖多组输入,常见注解有 @ParameterizedTest、@ValueSource、@CsvSource、@MethodSource。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ParameterizedTest
@ValueSource(strings = {"", " ", "abc"})
void testBlankOrNot(String input) {
boolean result = MyUtils.isBlank(input);
// 根据输入期望断言(示例省略)
}
异常测试与超时测试
要验证方法在异常情况下的行为,用 assertThrows;要限制执行时间,用 timeout。
@Test
void shouldThrow() {
Exception ex = assertThrows(IllegalArgumentException.class, () -> {
Utils.parseInt("x");
});
assertTrue(ex.getMessage().contains("invalid"));
}
@Test
void shouldFinishQuickly() {
assertTimeout(Duration.ofMillis(100), () -> {
// 受测代码
});
}
场景复杂时:嵌套测试、标签与条件执行
当测试场景很多时,可以用 @Nested 按上下文分组;用 @Tag 做过滤;用 @EnabledOnOs、@DisabledIf 环境条件化测试。
集成构建工具与 CI
把测试纳入构建和 CI 是关键步骤:
- 在 Maven/Gradle 中确保 test 阶段会运行 JUnit 平台。
- 在 CI(如 GitLab CI、Jenkins、GitHub Actions)中运行 mvn test 或 gradle test,并把测试报告(JUnit XML)导出,便于聚合报告。
- 对 flaky(间歇失败)测试做标记和分析,优先稳定或隔离。
与 Mockito 等模拟库配合(基础示例)
单元测试时常需模拟外部依赖,Mockito 是常用库。下面给出最基础的示例:
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
class ServiceTest {
@Test
void testWithMock() {
Repository repo = mock(Repository.class);
when(repo.find("id")).thenReturn(new Item("id"));
Service s = new Service(repo);
assertEquals("id", s.getItemId("id"));
}
}
*说明:* 模拟库能让你把测试限制在最小单元,不依赖数据库或网络。
常见陷阱与调试技巧(你可能会踩的坑)
- 测试不被识别:检查是否使用了 useJUnitPlatform() 或正确的 surefire/failsafe 配置。
- 忘记清理全局状态:导致测试间互相影响,优先改为无状态或在 @AfterEach 清理。
- 测试太多断言且互相依赖:失败时难以定位,拆分或使用 assertAll。
- 依赖真实外部资源:将其替换为模拟或集成测试分离执行。
断言与测试覆盖度的平衡
覆盖率不是目的,质量才是。高覆盖率的测试若断言薄弱也没用。写测试时关注关键逻辑分支和边界条件,而不是把每行都盲目测试。
表格:什么时候写单元测试,什么时候做集成测试?
| 类型 | 关注点 | 举例 |
| 单元测试 | 函数/类逻辑、快速、无外部依赖 | 字符串处理、计算、算法 |
| 集成测试 | 模块间交互、数据库/网络真实依赖 | Repository 与数据库交互、API 调用链路 |
| 端到端测试 | 真实系统在用户场景下的行为 | 用户登录流程、支付流程 |
示范:完整的测试用例结构(小型项目示例)
一个看起来合理的测试包结构:
- src/main/java/com/example/ — 生产代码
- src/test/java/com/example/ — 单元测试
- src/test/resources/ — 测试用资源
每个类对应一个 Test 类,测试名描述行为,例如 transferShouldDecreaseSourceAndIncreaseDest 之类的命名,比模糊的 test1 更好。
写测试时的写作风格建议(费曼式表达)
把测试写成一个小故事:先描述情景(given),再执行行为(when),最后验证结果(then)。如果你能给同事用一句话解释这个测试在验证什么,说明写得清楚。
调试失败的测试:快速排查流程
- 阅读失败断言与堆栈信息,定位失败点。
- 把复杂的断言拆分打印实际值或用 assertAll。
- 确认测试环境(系统属性、时间区域、随机种子)是否稳定。
- 如果怀疑并发问题,尝试把并发降为串行或用重试定位。
最后说点实践中的小细节(真心话)
你会发现,一开始测试都是用来保证新功能不回归,后来慢慢成了代码改动的安全网。写测试不是一次性完成的任务,它会随代码演进而被重构和修剪。别把测试当成负担,把它当成一种帮助你快速改动而不出意外的保险。
有时候我自己也会偷懒,先写最简单的断言,然后随着需求把测试补全;这其实挺自然 —— 先让工程跑起来,再慢慢把质量打磨好。这篇文章就到这儿,边写边想的感觉你应该能体会到,实践中遇到问题随时回头看看这些小技巧。