当前位置: 首页 > backend >正文

Maven + JUnit:Java单元测试的坚实组合

Maven + JUnit:Java单元测试的坚实组合

  • Maven + JUnit:Java单元测试的坚实组合
    • 一、什么是软件测试?
    • 二、测试的维度:阶段与方法
      • (一)测试的四大阶段
      • (二)测试的三大方法
    • 三、main方法测试与JUnit测试区别的对比
    • 四、使用IDEA进行单元测试
      • 1. ​​引入依赖​​
      • 2. ​​创建测试类与测试方法​​
    • 五、断言:单元测试的核心
      • (一)JUnit 断言方法参考表
        • 1. 基础断言方法
        • 2. 对象引用断言
        • 3. 异常断言
        • 4. 集合和数组断言
        • 5. 组合断言
        • 6. 超时断言
        • 7. 失败断言
    • 六、常见注解
    • 七、测试覆盖率
      • (一)什么是测试覆盖率?
      • (二)覆盖率数据总览
        • 1. `HelloWorld` 类分析
        • 2. `UserService` 类分析
    • 八、Maven依赖Scope标签
      • Scope的作用
      • 常用Scope类型对比
    • 九、总结

Maven + JUnit:Java单元测试的坚实组合

在软件开发的世界里,编写代码只是完成了上半场,而确保代码正确、稳定、高效地运行,则同样重要的下半场——这就是软件测试的舞台。无论你是技术小白还是资深开发者,理解测试都是通往高质量软件的必经之路。今天,我们就来系统性地解析软件测试的"纵横"之道。

一、什么是软件测试?

简单来说,软件测试是一个系统性的过程,旨在通过运行软件来评估并提升其质量。它的核心目标是鉴定软件的:

  • 正确性:是否做了它应该做的事?
  • 完整性:功能是否完整无缺?
  • 安全性:能否抵御外部威胁?
  • 质量:性能、易用性等是否良好?

可以说,测试是产品质量的"守门员",是交付用户信任之前的关键工序。

二、测试的维度:阶段与方法

测试活动是系统化的过程,通常可从两个维度进行理解:一是纵向的测试阶段,体现测试过程的层次性与先后顺序;二是横向的测试方法,反映对待测对象的不同视角与关注点。二者紧密结合,共同构成完整的测试体系。

(一)测试的四大阶段

测试流程与开发阶段环环相扣,遵循V模型从左至右、自下而上的顺序,可分为四个主要层次,层层递进,确保问题尽早发现,降低修复成本:

  1. 单元测试 (Unit Testing)

    • 介绍:针对软件最小的可测试单位(如函数、类)进行验证。
    • 目的:确保每个代码单元的质量符合预期。
    • 执行者:通常由开发人员完成。
    • 常用方法:以白盒测试为主,关注代码逻辑和结构。
  2. 集成测试 (Integration Testing)

    • 介绍:将已通过单元测试的模块组合,测试接口及交互。
    • 目的:验证模块能否正确协同工作,检查数据传递、接口调用等问题。
    • 重点:暴露接口错误和集成缺陷。
    • 常用方法:多采用灰盒测试,兼顾部分内部结构和外部功能。
  3. 系统测试 (System Testing)

    • 介绍:对完整软件系统进行整体测试。
    • 目的:验证系统是否满足需求规格,包括功能、性能、安全等非功能性属性。
    • 执行者:通常由专业测试工程师执行。
    • 常用方法:以黑盒测试为主,从用户视角检验系统行为。
  4. 验收测试 (Acceptance Testing)

    • 介绍:基于用户需求和业务场景进行的最终测试。
    • 目的:确认系统达到用户验收标准,可否正式交付使用。
    • 执行者客户、产品经理或终端用户。
    • 常用方法:属于黑盒测试,强调真实环境下的系统表现。

(二)测试的三大方法

根据对系统内部结构的了解程度,测试方法可分为三类:

  1. 白盒测试 (White-Box Testing)

    • 特点:如同具备“透视眼”,测试人员清楚代码内部逻辑与结构。
    • 关注点:代码覆盖、路径执行、内部逻辑正确性。
    • 典型应用:主要用于单元测试和部分集成测试。
  2. 黑盒测试 (Black-Box Testing)

    • 特点:从“用户视角”出发,不考虑程序内部实现,只关注输入与输出。
    • 关注点:功能符合性、需求实现、用户体验。
    • 典型应用:适用于系统测试验收测试
  3. 灰盒测试 (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 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

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 ? "男" : "女";}}

编写测试方法

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;/*** 测试类*/
@DisplayName("用户信息测试类")
public class UserServiceTest {@Testpublic 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)检查条件是否为trueassertTrue(list.contains("item"))
assertFalse(condition)检查条件是否为falseassertFalse(list.isEmpty())
assertNull(object)检查对象是否为nullassertNull(optionalValue.orElse(null))
assertNotNull(object)检查对象是否不为nullassertNotNull(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("测试不应执行到此点")

示例:

    /*** 断言*/@Testpublic void testGenderWithAssert(){UserService userService = new UserService();String gender = userService.getGender("100000200010011011");//断言//Assertions.assertEquals("男", gender);Assertions.assertEquals("男", gender, "性别获取错误有问题");}/*** 断言*/@Testpublic void testGenderWithAssert2(){UserService userService = new UserService();//断言Assertions.assertThrows(IllegalArgumentException.class, () -> {userService.getGender(null);});}

六、常见注解

注解说明备注
@Test测试类中的方法用它修饰才能成为测试方法,才能启动执行单元测试
@ParameterizedTest参数化测试的注解(可以让单个测试运行多次,每次运行时仅参数不同)用了该注解,就不需要@Test注解了
@ValueSource参数化测试的参数来源,赋予测试方法参数与参数化测试注解配合使用
@DisplayName指定测试类、测试方法显示的名称(默认为类名、方法名)
@BeforeEach用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次初始化资源(准备工作)
@AfterEach用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次释放资源(清理工作)
@BeforeAll用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次初始化资源(准备工作)
@AfterAll用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次释放资源(清理工作)

示例:

    // 测试配置@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里,我们可以选择使用覆盖率运行,这样我们就可以得到我们测试覆盖率

(二)覆盖率数据总览

类名类覆盖率方法覆盖率行覆盖率分支覆盖率
HelloWorld0% (0/1)0% (0/1)0% (0/1)0% (0/1)
UserService100%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范围(最常用)

<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依赖)高效管理测试生命周期。

​​测试覆盖率​​(如行覆盖、分支覆盖)为我们提供了量化测试有效性的重要视角,帮助我们洞察测试盲区(如未覆盖的方法或分支),但它仅是衡量测试充分性的一个维度,而非质量本身的唯一标准。

总而言之,一套成熟、高效的测试策略,是构建稳健、可维护软件系统的基石。它将验证与调试工作前置并自动化,最终为我们交付产品的信心保驾护航。

http://www.xdnf.cn/news/19568.html

相关文章:

  • MYSQL 认识事务
  • 大数据生态系统全景图:Hadoop、Spark、Flink、Hive、Kafka 的关系
  • three.js手机端的4种旋转方式
  • 优秀开源内容转自公众号后端开发成长指南
  • Java-114 深入浅出 MySQL 开源分布式中间件 ShardingSphere 深度解读
  • Linux 文本处理实战手册
  • 销售事业十年规划,并附上一套能帮助销售成长的「软件工具组合」
  • 爬虫实战练习
  • C 基础(1) - 初识C语言
  • 2025年数字化转型关键证书分析与选择指南
  • compile_commands.json 文件详解
  • Linux基础2
  • (3dnr)多帧视频图像去噪 (一)
  • GDAL 简介
  • C++ multiset数据结构的使用情况说明
  • 基于单片机智能饮水机/智能热水壶
  • 正式发布!2025AI SEO公司哪家专业?
  • 【数据分享】多份土地利用矢量shp数据分享-澳门
  • C# FlaUI win 自动化框架,介绍
  • 员工自愿放弃社保,企业给补贴合法吗?
  • Vue3 中 Proxy 在组件封装中的妙用
  • Windows 使用 Compass 访问MongoDb
  • 【HarmonyOS】一步解决弹框集成-快速弹框QuickDialog使用详解
  • 笔记:现代操作系统:原理与实现(1)
  • 卷积神经网络中的两个重要概念——感受野receptive filed和损失函数loss function
  • 【Element Plus `el-select` 下拉菜单响应式定位问题深度解析】
  • 刘洋洋《一笔相思绘红妆》上线,献给当代痴心人的一封情书
  • CUDA编程11 - CUDA异步执行介绍
  • Java 不支持在非静态内部类中声明静态 Static declarations in inner classes are not supported异常处理
  • elasticsearch中文分词器analysis-ik使用