Maven + JUnit:Java单元测试的坚实组合
- [Maven + JUnit:Java单元测试的坚实组合](#Maven + JUnit:Java单元测试的坚实组合)
-
- 一、什么是软件测试?
- 二、测试的维度:阶段与方法
- 三、main方法测试与JUnit测试区别的对比
- 四、使用IDEA进行单元测试
-
- [1. 引入依赖](#1. 引入依赖)
- [2. 创建测试类与测试方法](#2. 创建测试类与测试方法)
- 五、断言:单元测试的核心
-
- [(一)JUnit 断言方法参考表](#(一)JUnit 断言方法参考表)
-
- [1. 基础断言方法](#1. 基础断言方法)
- [2. 对象引用断言](#2. 对象引用断言)
- [3. 异常断言](#3. 异常断言)
- [4. 集合和数组断言](#4. 集合和数组断言)
- [5. 组合断言](#5. 组合断言)
- [6. 超时断言](#6. 超时断言)
- [7. 失败断言](#7. 失败断言)
- 六、常见注解
- 七、测试覆盖率
-
- (一)什么是测试覆盖率?
- (二)覆盖率数据总览
-
- [1. `HelloWorld` 类分析](#1.
HelloWorld
类分析) - [2. `UserService` 类分析](#2.
UserService
类分析)
- [1. `HelloWorld` 类分析](#1.
- 八、Maven依赖Scope标签
- 九、总结
Maven + JUnit:Java单元测试的坚实组合
在软件开发的世界里,编写代码只是完成了上半场,而确保代码正确、稳定、高效地运行,则同样重要的下半场------这就是软件测试的舞台。无论你是技术小白还是资深开发者,理解测试都是通往高质量软件的必经之路。今天,我们就来系统性地解析软件测试的"纵横"之道。
一、什么是软件测试?
简单来说,软件测试是一个系统性的过程 ,旨在通过运行软件来评估并提升其质量。它的核心目标是鉴定软件的:
- 正确性:是否做了它应该做的事?
- 完整性:功能是否完整无缺?
- 安全性:能否抵御外部威胁?
- 质量:性能、易用性等是否良好?
可以说,测试是产品质量的"守门员",是交付用户信任之前的关键工序。
二、测试的维度:阶段与方法
测试活动是系统化的过程,通常可从两个维度进行理解:一是纵向的测试阶段,体现测试过程的层次性与先后顺序;二是横向的测试方法,反映对待测对象的不同视角与关注点。二者紧密结合,共同构成完整的测试体系。
(一)测试的四大阶段
测试流程与开发阶段环环相扣,遵循V模型从左至右、自下而上的顺序,可分为四个主要层次,层层递进,确保问题尽早发现,降低修复成本:
-
单元测试 (Unit Testing)
- 介绍:针对软件最小的可测试单位(如函数、类)进行验证。
- 目的:确保每个代码单元的质量符合预期。
- 执行者 :通常由开发人员完成。
- 常用方法 :以白盒测试为主,关注代码逻辑和结构。
-
集成测试 (Integration Testing)
- 介绍:将已通过单元测试的模块组合,测试接口及交互。
- 目的:验证模块能否正确协同工作,检查数据传递、接口调用等问题。
- 重点:暴露接口错误和集成缺陷。
- 常用方法 :多采用灰盒测试,兼顾部分内部结构和外部功能。
-
系统测试 (System Testing)
- 介绍:对完整软件系统进行整体测试。
- 目的:验证系统是否满足需求规格,包括功能、性能、安全等非功能性属性。
- 执行者 :通常由专业测试工程师执行。
- 常用方法 :以黑盒测试为主,从用户视角检验系统行为。
-
验收测试 (Acceptance Testing)
- 介绍:基于用户需求和业务场景进行的最终测试。
- 目的:确认系统达到用户验收标准,可否正式交付使用。
- 执行者 :客户、产品经理或终端用户。
- 常用方法 :属于黑盒测试,强调真实环境下的系统表现。
(二)测试的三大方法
根据对系统内部结构的了解程度,测试方法可分为三类:
-
白盒测试 (White-Box Testing)
- 特点:如同具备"透视眼",测试人员清楚代码内部逻辑与结构。
- 关注点:代码覆盖、路径执行、内部逻辑正确性。
- 典型应用 :主要用于单元测试和部分集成测试。
-
黑盒测试 (Black-Box Testing)
- 特点:从"用户视角"出发,不考虑程序内部实现,只关注输入与输出。
- 关注点:功能符合性、需求实现、用户体验。
- 典型应用 :适用于系统测试 与验收测试。
-
灰盒测试 (Gray-Box Testing)
- 特点:"半透视"方式,既了解部分内部结构(如数据库、接口),也结合外部功能验证。
- 关注点:接口规范、数据流、集成逻辑。
- 典型应用 :常见于集成测试、安全测试和性能测试。
三、main方法测试与JUnit测试区别的对比
特性维度 | Main方法测试 | JUnit测试 |
---|---|---|
本质 | 一种临时 、原始 的测试手段,在main 函数中编写测试代码。 |
一个专业 、标准化的单元测试框架。 |
编写方式 | 需要手动编写main 方法,并在其中创建对象、调用方法、打印结果。 |
使用注解(如@Test )标识测试方法,框架自动识别和执行。 |
执行方式 | 手动运行整个main 方法,或作为应用的入口点启动。 |
由JUnit测试运行器自动执行,可以单独运行一个测试方法、一个测试类或整个测试套件。 |
结果验证 | 人工观察 控制台输出,与预期结果进行肉眼比对。 | 使用断言(Assertions) API(如assertEquals , assertTrue )进行自动化验证。 |
可读性与组织 | 差。多个测试用例混杂在一起,逻辑混乱,不易维护和管理。 | 好。每个测试方法独立且聚焦,测试类结构清晰,易于组织和管理。 |
测试隔离 | 差。测试用例通常按顺序执行,一个用例的失败或异常可能导致后续用例中断。 | 好。JUnit为每个@Test 方法创建一个新的测试实例,确保测试之间的独立性。 |
测试生命周期 | 无。需要手动完成 setup(准备)和 teardown(清理)操作。 | 提供注解(如@BeforeEach , @AfterEach )来管理测试的前后置操作,生命周期清晰。 |
效率 | 低。大量重复代码,验证效率低下,无法实现自动化回归测试。 | 高。编写一次,可一键运行所有测试,是持续集成(CI) 和自动化测试的基石。 |
功能支持 | 功能单一,仅支持最基本的验证。 | 功能强大,支持参数化测试、重复测试、超时测试、测试套件等高级特性。 |
适用场景 | 快速验证一小段代码的简单逻辑,临时性、一次性的检查。 | 所有正式的单元测试场景 ,是软件开发中不可或缺的工程质量保障环节。 |
四、使用IDEA进行单元测试
1. 引入依赖
在 pom.xml 配置文件中,引入 JUnit 的依赖
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Maventest1</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 核心JUnit依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
2. 创建测试类与测试方法
在 src/test/java 目录下,创建对应的测试类,并编写测试方法。在每个测试方法上声明 @Test 注解。
创建对应的测试类UserService
java
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
public class UserService {
/**
* 给定一个身份证号, 计算出该用户的年龄
* @param idCard 身份证号
*/
public Integer getAge(String idCard){
if (idCard == null || idCard.length() != 18) {
throw new IllegalArgumentException("无效的身份证号码");
}
String birthday = idCard.substring(6, 14);
LocalDate parse = LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyyMMdd"));
return Period.between(parse, LocalDate.now()).getYears();
}
/**
* 给定一个身份证号, 计算出该用户的性别
* @param idCard 身份证号
*/
public String getGender(String idCard){
if (idCard == null || idCard.length() != 18) {
throw new IllegalArgumentException("无效的身份证号码");
}
return Integer.parseInt(idCard.substring(16,17)) % 2 == 1 ? "男" : "女";
}
}
编写测试方法
java
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
/**
* 测试类
*/
@DisplayName("用户信息测试类")
public class UserServiceTest {
@Test
public void testGetAge(){
UserService userService = new UserService();
Integer age = userService.getAge("100000200010011011");
System.out.println(age);
}
}
以下是测试结果

命名规范要求:
测试类名采用:XxxxTest(规范)
测试方法格式:public void xxxx(){...}(规定)
五、断言:单元测试的核心
在Java单元测试中,断言(Assertion)是验证代码行为是否符合预期的核心机制。通过断言,我们能够明确地定义测试的预期结果,并在实际结果与预期不符时立即发现问题。
在测试的时候,代码可以跑只能说明我们的测试代码没有错误,不能代表我们测试的方法逻辑没有错误
(一)JUnit 断言方法参考表
1. 基础断言方法
断言方法 | 描述 | 示例 |
---|---|---|
assertEquals(expected, actual) |
检查两个值是否相等 | assertEquals(5, calculator.add(2, 3)) |
assertEquals(expected, actual, message) |
检查两个值是否相等,带自定义错误消息 | assertEquals(5, result, "加法计算结果错误") |
assertEquals(expected, actual, delta) |
检查两个浮点数在误差范围内是否相等 | assertEquals(3.14, pi, 0.01) |
assertNotEquals(unexpected, actual) |
检查两个值是否不相等 | assertNotEquals(0, calculator.divide(5, 2)) |
assertTrue(condition) |
检查条件是否为true | assertTrue(list.contains("item")) |
assertFalse(condition) |
检查条件是否为false | assertFalse(list.isEmpty()) |
assertNull(object) |
检查对象是否为null | assertNull(optionalValue.orElse(null)) |
assertNotNull(object) |
检查对象是否不为null | assertNotNull(user.getName()) |
2. 对象引用断言
断言方法 | 描述 | 示例 |
---|---|---|
assertSame(expected, actual) |
检查两个对象引用是否指向同一个对象 | assertSame(singleton1, singleton2) |
assertNotSame(unexpected, actual) |
检查两个对象引用是否指向不同对象 | assertNotSame(new Object(), new Object()) |
3. 异常断言
断言方法 | 描述 | 示例 |
---|---|---|
assertThrows(exceptionType, executable) |
检查代码是否抛出指定类型的异常 | assertThrows(IllegalArgumentException.class, () -> method(null)) |
assertThrows(exceptionType, executable, message) |
检查代码是否抛出指定类型的异常,带自定义错误消息 | assertThrows(IOException.class, () -> readFile(), "应该抛出IO异常") |
assertDoesNotThrow(executable) |
检查代码是否不抛出任何异常 | assertDoesNotThrow(() -> validMethod()) |
4. 集合和数组断言
断言方法 | 描述 | 示例 |
---|---|---|
assertArrayEquals(expected, actual) |
检查两个数组是否相等 | assertArrayEquals(new int[]{1,2}, new int[]{1,2}) |
assertIterableEquals(expected, actual) |
检查两个可迭代对象是否相等 | assertIterableEquals(Arrays.asList("a","b"), list) |
assertLinesMatch(expected, actual) |
检查两个字符串列表是否匹配 | assertLinesMatch(expectedLines, outputLines) |
5. 组合断言
断言方法 | 描述 | 示例 |
---|---|---|
assertAll(heading, executables...) |
执行多个断言,报告所有失败 | assertAll("用户属性", () -> assertEquals("John", user.firstName), () -> assertEquals("Doe", user.lastName)) |
assertAll(executables...) |
执行多个断言,报告所有失败 | assertAll(() -> assertTrue(x > 0), () -> assertTrue(y > 0)) |
6. 超时断言
断言方法 | 描述 | 示例 |
---|---|---|
assertTimeout(duration, executable) |
检查代码是否在指定时间内完成 | assertTimeout(Duration.ofMillis(100), () -> fastOperation()) |
assertTimeoutPreemptively(duration, executable) |
检查代码是否在指定时间内完成,超时立即终止 | assertTimeoutPreemptively(Duration.ofSeconds(1), () -> potentiallyLongOperation()) |
7. 失败断言
断言方法 | 描述 | 示例 |
---|---|---|
fail() |
直接使测试失败 | if (condition) fail() |
fail(message) |
直接使测试失败,带自定义消息 | fail("测试不应执行到此点") |
示例:
java
/**
* 断言
*/
@Test
public void testGenderWithAssert(){
UserService userService = new UserService();
String gender = userService.getGender("100000200010011011");
//断言
//Assertions.assertEquals("男", gender);
Assertions.assertEquals("男", gender, "性别获取错误有问题");
}
/**
* 断言
*/
@Test
public void testGenderWithAssert2(){
UserService userService = new UserService();
//断言
Assertions.assertThrows(IllegalArgumentException.class, () -> {
userService.getGender(null);
});
}
六、常见注解
注解 | 说明 | 备注 |
---|---|---|
@Test |
测试类中的方法用它修饰才能成为测试方法,才能启动执行 | 单元测试 |
@ParameterizedTest |
参数化测试的注解(可以让单个测试运行多次,每次运行时仅参数不同) | 用了该注解,就不需要@Test注解了 |
@ValueSource |
参数化测试的参数来源,赋予测试方法参数 | 与参数化测试注解配合使用 |
@DisplayName |
指定测试类、测试方法显示的名称(默认为类名、方法名) | |
@BeforeEach |
用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次 | 初始化资源(准备工作) |
@AfterEach |
用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次 | 释放资源(清理工作) |
@BeforeAll |
用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次 | 初始化资源(准备工作) |
@AfterAll |
用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次 | 释放资源(清理工作) |
示例:
java
// 测试配置
@DisplayName("测试用户性别")
@ParameterizedTest
@ValueSource(strings = {"100000200010011011", "100000200010011031", "100000200010011051"})
// 测试方法
public void testGetGender2(String idCard) {
// 初始化被测服务
UserService userService = new UserService();
// 调用被测方法
String gender = userService.getGender(idCard);
// 断言:验证返回结果是否为"男"
Assertions.assertEquals("男", gender);
}

七、测试覆盖率
(一)什么是测试覆盖率?
测试覆盖率是衡量测试代码对源代码覆盖程度的指标,它反映了测试的全面性和有效性。高覆盖率并不意味着没有bug,但低覆盖率通常意味着测试不充分。

在IDEA里,我们可以选择使用覆盖率运行,这样我们就可以得到我们测试覆盖率
(二)覆盖率数据总览
类名 | 类覆盖率 | 方法覆盖率 | 行覆盖率 | 分支覆盖率 |
---|---|---|---|---|
HelloWorld |
0% (0/1) | 0% (0/1) | 0% (0/1) | 0% (0/1) |
UserService |
100% | 37% (3/8) | 60% (6/10) | 70% |
1. HelloWorld
类分析
- 各项覆盖率均为 0%
- 结论:该类完全未被测试,是需要重点关注和改进的测试盲区
2. UserService
类分析
- 类覆盖率 100%:测试已正确实例化该类
- 方法覆盖率 37% (3/8):8个方法中只有3个被测试,存在严重遗漏
- 行覆盖率 60% (6/10):10行可执行代码中有4行未执行
- 分支覆盖率 70%:10个分支点中有3个未被覆盖
八、Maven依赖Scope标签
Scope的作用
控制依赖项的作用范围 和生命周期阶段,决定依赖在哪些阶段被引入classpath
常用Scope类型对比
Scope类型 | 作用范围 | 是否参与打包 | 典型应用场景 |
---|---|---|---|
compile | 所有阶段 | ✔️ | 核心依赖(Spring, Hibernate) |
test | 测试阶段 | ✘ | 测试框架(JUnit, Mockito) |
provided | 编译/测试 | ✘ | 容器提供依赖(Servlet API) |
runtime | 运行/测试 | ✔️ | JDBC驱动(mysql-connector) |
system | 编译/测试 | ✔️ | 本地系统库(谨慎使用) |
import | 依赖管理 | - | 多模块依赖管理 |
test范围(最常用)
xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
九、总结
软件测试是保障软件质量与可靠性的核心实践。它贯穿于整个开发生命周期,从单元测试、集成测试到系统测试和验收测试(V模型),运用黑盒、白盒与灰盒等测试方法,构建起一个多层次、多角度的质量保障体系。
在实践中,我们已从原始的main方法测试,演进到使用JUnit等专业化测试框架。通过丰富的断言(assertEquals, assertThrows等)和注解(@Test, @BeforeEach等),我们能够以更简洁、规范且自动化的方式编写用例,并利用Maven(配合test scope依赖)高效管理测试生命周期。
测试覆盖率(如行覆盖、分支覆盖)为我们提供了量化测试有效性的重要视角,帮助我们洞察测试盲区(如未覆盖的方法或分支),但它仅是衡量测试充分性的一个维度,而非质量本身的唯一标准。
总而言之,一套成熟、高效的测试策略,是构建稳健、可维护软件系统的基石。它将验证与调试工作前置并自动化,最终为我们交付产品的信心保驾护航。