JavaWeb 开发—— Maven使用与JUnit单元测试
一、什么是单元测试?
单元测试,就是针对代码里最小的可测试功能单元(单个方法),编写独立的测试代码,验证这个方法的输入、输出、逻辑是否完全符合设计预期,给每个方法写对应的测试代码,就是单元测试。
二、为什么不用 main 方法?
main 方法测试的痛点:代码耦合,难以维护:测试代码和业务源代码混写在同一个类里,上线时需要手动清理,代码混乱,长期维护成本极高;测试不独立,连锁失败:所有测试逻辑都写在一个 main 方法里,前面的代码报错、抛出异常,后面的所有测试逻辑直接终止执行;无自动化能力,无标准化报告:只能手动运行 main 方法,靠人判断结果,无法做自动化测试,也无法自动统计。
三、JUnit 搭配 Maven 使用
1:在pom.xml中引入JUnit
通过<dependency>标签,自动下载引入JUnit的jar包,不用手动导入文件(前面已经提到是通过GAV实现的)。
<!-- JUnit 5 核心依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.1</version>
</dependency>
2:创建测试方法
Maven项目有固定的规范目录结构:
- 业务主代码放在
src/main/java目录下; - 测试代码固定放在
src/test/java目录下:
测试类UserServiceTest.java:
@Test //必须引入Test,标记这个方法是JUnit单元测试方法
public void testGetAge(){
// 1. 调用业务类UserService的getAge方法,传入身份证号,获取计算出的年龄
Integer age = new UserService().getAge("110002200505091218");
// 2. 打印结果,人工验证计算出的年龄是否符合预期
System.out.println(age);
}
@Test:JUnit的注解和public void的无参方法,标记为可独立运行的单元测试方法,能单独运行测试。- 点击运行可以看到测试是否通过,多个测试函数可以看到每一个的具体情况:

3:引入断言
为了进一步验证代码结果是否正确,引入 JUnit 中的断言功能:
public class Assertions {
/**
* 检查两个值是否相等,不相等就报错
* @param expected 预期值
* @param actual 实际值
* @param message 【可选】错误提示信息
*/
public static void assertEquals(Object expected, Object actual, String message) {}
/**
* 检查两个值是否不相等,相等就报错
* @param unexpected 不期望的值
* @param actual 实际值
* @param message 【可选】错误提示信息
*/
public static void assertNotEquals(Object unexpected, Object actual, String message) {}
/**
* 检查对象是否为null,不为null就报错
* @param actual 待检查对象
* @param message 【可选】错误提示信息
*/
public static void assertNull(Object actual, String message) {}
/**
* 检查对象是否不为null,为null就报错
* @param actual 待检查对象
* @param message 【可选】错误提示信息
*/
public static void assertNotNull(Object actual, String message) {}
/**
* 检查条件是否为true,不为true就报错
* @param condition 布尔条件
* @param message 【可选】错误提示信息
*/
public static void assertTrue(boolean condition, String message) {}
/**
* 检查条件是否为false,不为false就报错
* @param condition 布尔条件
* @param message 【可选】错误提示信息
*/
public static void assertFalse(boolean condition, String message) {}
/**
* 检查程序运行抛出的异常是否符合预期
* @param expectedType 预期异常类型
* @param executable 待执行代码块
* @param message 【可选】错误提示信息
* @param <T> 异常类型泛型
* @return 抛出的异常对象
*/
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable, String message) {
return null;
}
}
@Test
void testAdd() {
// 格式:assertEquals(预期值, 实际值, 错误提示信息)
assertEquals(expectedResult, actualResult, "3 + 5 应该等于 8");
System.out.println("测试通过:assertEquals 验证成功");
}
@Test
void testDivideByZero() {
// 格式:assertThrows(预期异常类型, 执行代码块)
// 它会返回抛出的异常对象,方便我们后续验证异常信息
ArithmeticException exception = assertThrows(
ArithmeticException.class, // 预期会抛出的异常类型
() -> calculator.divide(dividend, divisor), // 这里写会抛出异常的代码
"除以 0 时应该抛出 ArithmeticException"
);
}
4:引入注释
下面是JUnit中的常见注释,引入
| 注解 | 说明 | 备注 |
|---|---|---|
| @Test | 测试类中的方法用它修饰才能成为测试方法,才能启动执行 | 单元测试 |
| @ParameterizedTest | 参数化测试的注解(可以让单个测试运行多次,每次运行时仅参数不同) | 用了该注解,就不需要@Test注解了 |
| @ValueSource | 参数化测试的参数来源,赋予测试方法参数 | 与参数化测试注解配合使用 |
| @DisplayName | 指定测试类、测试方法显示的名称 (默认为类名、方法名) | |
| @BeforeEach | 用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。 | 初始化资源(准备工作) |
| @AfterEach | 用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。 | 释放资源(清理工作) |
| @BeforeAll | 用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。 | 初始化资源(准备工作) |
| @AfterAll | 用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。 | 释放资源(清理工作) |
常规使用示例:
public class LifecycleAnnotationDemo {
@BeforeAll //在所有测试前执行
static void initAll() {//一定要写static
// 场景:比如连接数据库、启动服务器这种非常耗时的操作
// 只需要在所有测试开始前做 1 次
//。。。
}
@BeforeEach //在每一次测试前执行
void init() {
// 场景:比如打开浏览器、新建一个测试用的对象
// 每个 @Test 方法运行前,都会重新执行一次
//。。。
}
@AfterEach //每一次测试后执行
void tearDown() {
// 场景:比如关闭浏览器、删除测试产生的临时数据
// 每个 @Test 方法运行后,都会执行一次
//。。。
}
@AfterAll //在所有测试之后运行
static void tearDownAll() { //一定是static
// 场景:比如断开数据库连接、关闭服务器
// 只需要在所有测试结束后做 1 次
//。。。
}
}
参数化测试:(省去了一个一个参数单独测试的麻烦)
// 这是 JUnit 定义的注解类,提供了ints、strings等定义
public @interface ValueSource {
// 这里定义了一个属性,名字叫 strings,类型是 String 数组
String[] strings() default {};
// 这里定义了一个属性,名字叫 ints,类型是 int 数组
int[] ints() default {};
// 这里定义了一个属性,名字叫 longs,类型是 long 数组
long[] longs() default {};
// 这里定义了一个属性,名字叫 doubles,类型是 double 数组
double[] doubles() default {};
// ... 还有其他类型
}
public class NumberUtilTest {
/**
* 参数化测试演示
* 1. 用 @ParameterizedTest 代替 @Test
* 2. 用 @ValueSource 传入一堆参数
*/
@DisplayName("测试名称")
@ParameterizedTest // 注意:这里不是 @Test
@ValueSource(ints = {2, 4, 0, -6}) // 提供一组 int 类型的参数,这里的ints代表int数组
void testIsEvenWithTrueCases(int number) {
// 这个方法会运行 4 次,每次 number 分别等于 2, 4, 0, -6
System.out.println("正在测试参数:" + number);
// 断言:这些数都应该返回 true
assertTrue(NumberUtil.isEven(number));
}
}
只有参数化测试(@ParameterizedTest)能接收形参(如 @ValueSource 里的数字)。
在实际生产环境中使用JUnit进行单元测试时,应该尽可能考虑到所有可能发生的情况,不仅要测试正确的测试用例,还要将所有可能出现的测试用例都要进行测试,保证代码的完整性和健全性!
5:覆盖率测试
点击测试类使用覆盖率运行“”,即可看到测试代码的覆盖率情况:

覆盖率各个属性的含义如下:
-
类(%) → 类覆盖率:统计有多少个类,被单元测试触达执行过。只要这个类里的任意一行可执行代码被测试跑过,这个类就算被覆盖了。
-
方法(%) → 方法覆盖率:统计类里有多少个方法,被单元测试触达执行过。只要方法里的任意一行可执行代码被跑过,这个方法就算被覆盖;空方法、接口方法、抽象方法不会被计入统计。
-
行(%) → 行覆盖率:统计代码里,有多少行可执行的逻辑代码,被单元测试执行过。
- 不是所有行都会被统计:注释、空行、类定义、方法定义、单纯的变量声明(比如
int a;),都不会计入分母。真正会被执行的逻辑(赋值、计算、方法调用、条件判断等)才会被统计。 - 运行覆盖率后,你的业务代码左侧会有颜色标记:
绿色= 这行代码被测试完全覆盖了红色= 这行代码完全没被测试执行过黄色= 这行代码部分覆盖(比如if条件只走了一个分支)
- 不是所有行都会被统计:注释、空行、类定义、方法定义、单纯的变量声明(比如
-
分支(%) → 分支覆盖率:统计代码里的所有逻辑分支,有多少被单元测试覆盖到了。这里的分支,指的是
if/else、switch、三元运算符、for/while的条件判断等所有会产生不同执行路径的逻辑。- 如果测试只跑了if成立的情况,分支覆盖率就是50%;
- 只有if和else都被测试执行过,分支覆盖率才是100%;
更多推荐
所有评论(0)