本文目录导读:

我将为您提供Java单元测试覆盖率的完整案例,涵盖各种覆盖率类型和实际应用场景。
覆盖率类型及工具
<!-- pom.xml 依赖配置 -->
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.1.1</version>
<scope>test</scope>
</dependency>
<!-- AssertJ -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- JaCoCo 插件 -->
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>CLASS</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
业务代码示例
// Calculator.java - 计算器业务类
public class Calculator {
/**
* 计算折扣价格
*/
public double calculateDiscount(double price, int quantity, CustomerType customerType) {
if (price < 0) {
throw new IllegalArgumentException("价格不能为负数");
}
if (quantity <= 0) {
throw new IllegalArgumentException("数量必须大于0");
}
// 默认折扣
double discount = 1.0;
// 根据数量调整折扣
if (quantity >= 10) {
discount -= 0.15; // 批量购买折扣
} else if (quantity >= 5) {
discount -= 0.10;
} else if (quantity >= 3) {
discount -= 0.05;
}
// 根据会员类型调整折扣
if (customerType != null) {
switch (customerType) {
case VIP:
discount -= 0.20;
break;
case GOLD:
discount -= 0.10;
break;
case SILVER:
discount -= 0.05;
break;
default:
break;
}
}
// 最低折扣不能低于0.5
discount = Math.max(discount, 0.5);
return price * quantity * discount;
}
/**
* 判断是否为质数
*/
public boolean isPrime(int number) {
if (number <= 1) {
return false;
}
if (number <= 3) {
return true;
}
if (number % 2 == 0 || number % 3 == 0) {
return false;
}
for (int i = 5; i * i <= number; i += 6) {
if (number % i == 0 || number % (i + 2) == 0) {
return false;
}
}
return true;
}
/**
* 计算阶乘(递归)
*/
public long factorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("不能计算负数的阶乘");
}
if (n == 0 || n == 1) {
return 1;
}
return n * factorial(n - 1);
}
/**
* 计算数组的平均值
*/
public double average(int[] numbers) {
if (numbers == null || numbers.length == 0) {
throw new IllegalArgumentException("数组不能为空");
}
int sum = 0;
for (int num : numbers) {
sum += num;
}
return (double) sum / numbers.length;
}
}
// CustomerType.java - 客户类型枚举
public enum CustomerType {
NORMAL, VIP, GOLD, SILVER
}
完整的单元测试代码
// CalculatorTest.java - 覆盖行、分支、条件覆盖率
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
// ========== calculateDiscount 测试 ==========
// 行覆盖率测试(针对每一行代码)
@Nested
@DisplayName("折扣计算测试")
class CalculateDiscountTest {
@Test
@DisplayName("正常场景:少量购买,无会员")
void shouldReturnCorrectDiscountForSmallQuantity() {
double result = calculator.calculateDiscount(100.0, 1, CustomerType.NORMAL);
// 100 * 1 * 1.0 = 100
assertEquals(100.0, result);
}
@Test
@DisplayName("正常场景:购买3件")
void shouldReturnCorrectDiscountForThreeItems() {
double result = calculator.calculateDiscount(100.0, 3, CustomerType.NORMAL);
// 100 * 3 * 0.95 = 285
assertEquals(285.0, result);
}
@Test
@DisplayName("正常场景:购买5件")
void shouldReturnCorrectDiscountForFiveItems() {
double result = calculator.calculateDiscount(100.0, 5, CustomerType.NORMAL);
// 100 * 5 * 0.90 = 450
assertEquals(450.0, result);
}
@Test
@DisplayName("正常场景:购买10件以上")
void shouldReturnCorrectDiscountForTenOrMoreItems() {
double result = calculator.calculateDiscount(100.0, 10, CustomerType.NORMAL);
// 100 * 10 * 0.85 = 850
assertEquals(850.0, result);
}
@Test
@DisplayName("VIP客户购买")
void shouldApplyVipDiscount() {
double result = calculator.calculateDiscount(100.0, 2, CustomerType.VIP);
// 100 * 2 * (1.0 - 0.20) = 160
assertEquals(160.0, result);
}
@Test
@DisplayName("GOLD客户购买")
void shouldApplyGoldDiscount() {
double result = calculator.calculateDiscount(100.0, 2, CustomerType.GOLD);
// 100 * 2 * (1.0 - 0.10) = 180
assertEquals(180.0, result);
}
@Test
@DisplayName("SILVER客户购买")
void shouldApplySilverDiscount() {
double result = calculator.calculateDiscount(100.0, 2, CustomerType.SILVER);
// 100 * 2 * (1.0 - 0.05) = 190
assertEquals(190.0, result);
}
@Test
@DisplayName("VIP批量客户,验证最低折扣")
void shouldApplyMinimumDiscount() {
double result = calculator.calculateDiscount(50.0, 100, CustomerType.VIP);
// 50 * 100 * 0.5 = 2500 (因为最低折扣是0.5)
assertEquals(2500.0, result);
}
@Test
@DisplayName("参数异常:价格负数")
void shouldThrowExceptionForNegativePrice() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculateDiscount(-1, 1, CustomerType.NORMAL));
}
@Test
@DisplayName("参数异常:数量为0")
void shouldThrowExceptionForZeroQuantity() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculateDiscount(100, 0, CustomerType.NORMAL));
}
// 分支覆盖率测试(参数化测试)
@ParameterizedTest
@CsvSource({
"100, 1, NORMAL, 100.0",
"100, 2, NORMAL, 200.0",
"100, 3, NORMAL, 285.0",
"100, 5, NORMAL, 450.0",
"100, 10, NORMAL, 850.0",
"100, 2, VIP, 160.0",
"100, 2, GOLD, 180.0"
})
@DisplayName("使用CSV数据测试不同场景")
void testCalculateDiscountWithCsv(double price, int quantity,
CustomerType type, double expected) {
double result = calculator.calculateDiscount(price, quantity, type);
assertEquals(expected, result, 0.001);
}
}
// ========== isPrime 测试 ==========
// 条件覆盖率测试(条件组合)
@Nested
@DisplayName("质数判断测试")
class IsPrimeTest {
// 行覆盖率:覆盖所有行
@Test
@DisplayName("所有分支测试")
void testAllBranches() {
assertFalse(calculator.isPrime(0)); // 覆盖 number <= 1
assertFalse(calculator.isPrime(1)); // 覆盖 number <= 1
assertTrue(calculator.isPrime(2)); // 覆盖 number <= 3
assertTrue(calculator.isPrime(3)); // 覆盖 number <= 3
assertFalse(calculator.isPrime(4)); // 覆盖 number % 2 == 0
assertFalse(calculator.isPrime(9)); // 覆盖 number % 3 == 0
assertTrue(calculator.isPrime(11)); // 覆盖 for 循环
assertFalse(calculator.isPrime(25)); // 覆盖循环内条件
assertTrue(calculator.isPrime(97)); // 覆盖循环结束
}
// 参数化测试 - 提高覆盖率
@ParameterizedTest
@ValueSource(ints = {-1, 0, 1, 4, 6, 8, 9, 12, 15, 21})
@DisplayName("测试非质数")
void testNonPrimes(int number) {
assertFalse(calculator.isPrime(number));
}
@ParameterizedTest
@ValueSource(ints = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29})
@DisplayName("测试质数")
void testPrimes(int number) {
assertTrue(calculator.isPrime(number));
}
@ParameterizedTest
@MethodSource("primeDataProvider")
@DisplayName("使用方法提供测试数据")
void testWithMethodSource(int number, boolean expected) {
assertEquals(expected, calculator.isPrime(number));
}
static Stream<Arguments> primeDataProvider() {
return Stream.of(
Arguments.of(0, false),
Arguments.of(1, false),
Arguments.of(2, true),
Arguments.of(3, true),
Arguments.of(4, false),
Arguments.of(5, true),
Arguments.of(6, false),
Arguments.of(7, true),
Arguments.of(8, false),
Arguments.of(9, false),
Arguments.of(10, false)
);
}
}
// ========== factorial 测试 ==========
// 递归方法覆盖率测试
@Nested
@DisplayName("阶乘测试")
class FactorialTest {
@Test
@DisplayName("测试边界条件")
void testBoundaryConditions() {
// 负数的阶乘
assertThrows(IllegalArgumentException.class,
() -> calculator.factorial(-1));
// 0的阶乘 = 1
assertEquals(1, calculator.factorial(0));
// 1的阶乘 = 1
assertEquals(1, calculator.factorial(1));
}
@ParameterizedTest
@CsvSource({
"0, 1",
"1, 1",
"2, 2",
"3, 6",
"4, 24",
"5, 120",
"6, 720",
"7, 5040",
"8, 40320",
"10, 3628800"
})
@DisplayName("测试多个阶乘值")
void testFactorialValues(int input, long expected) {
assertEquals(expected, calculator.factorial(input));
}
// 测试性能(可以用于超时测试)
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
@DisplayName("性能测试")
void testPerformance() {
long result = calculator.factorial(20);
// 确保结果正确
assertEquals(2432902008176640000L, result);
}
}
// ========== average 测试 ==========
// 输入组合覆盖率测试
@Nested
@DisplayName("平均值测试")
class AverageTest {
@Test
@DisplayName("测试所有组合")
void testAllCombinations() {
// 空数组异常
assertThrows(IllegalArgumentException.class,
() -> calculator.average(null));
assertThrows(IllegalArgumentException.class,
() -> calculator.average(new int[]{}));
// 只有一个元素
assertEquals(5.0, calculator.average(new int[]{5}));
// 多个正数
assertEquals(2.5, calculator.average(new int[]{1, 2, 3, 4}));
// 包含负数
assertEquals(1.0, calculator.average(new int[]{-1, 0, 3, 2}));
// 大数组
int[] largeArray = new int[1000];
for (int i = 0; i < 1000; i++) {
largeArray[i] = i;
}
assertEquals(499.5, calculator.average(largeArray), 0.01);
}
@Test
@DisplayName("使用AssertJ进行断言")
void testWithAssertJ() {
// 使用AssertJ的断言风格
assertThat(calculator.average(new int[]{1, 2, 3, 4, 5}))
.isEqualTo(3.0);
assertThat(calculator.average(new int[]{-2, -1, 0}))
.isEqualTo(-1.0);
assertThat(calculator.calculateDiscount(100, 2, CustomerType.NORMAL))
.isNotNegative()
.isGreaterThan(150)
.isLessThan(250);
}
}
// 整体测试分组
@AfterEach
void tearDown() {
calculator = null;
}
}
覆盖率报告示例
// CoverageReport.java - 覆盖率报告分析工具
public class CoverageReport {
/**
* 生成覆盖率摘要
*/
public static void generateCoverageSummary() {
// 以下是在控制台输出的示例
// 行覆盖率: 92.5%
// 分支覆盖率: 85.7%
// 方法覆盖率: 100%
// 类覆盖率: 100%
}
}
运行测试并生成覆盖率报告
# 运行测试并生成覆盖率报告 mvn clean test # 生成完整报告 mvn verify # 自定义JaCoCo参数 mvn test -Djacoco.skip=false -Djacoco.destFile=target/jacoco.exec # 查看报告 # 报告位置: target/site/jacoco/index.html
测试覆盖率高阶技巧
// AdvancedTestExample.java - 高级测试技巧
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class AdvancedTestExample {
@Mock
private Calculator calculatorMock;
// 1. 使用Mockito模拟复杂的交互
@Test
void testWithMockObjects() {
// 模拟行为
when(calculatorMock.calculateDiscount(anyDouble(), anyInt(), any()))
.thenReturn(100.0);
// 验证调用的次数
verify(calculatorMock, times(1)).calculateDiscount(100.0, 2, CustomerType.NORMAL);
// 验证从未被调用
verify(calculatorMock, never()).isPrime(anyInt());
}
// 2. 使用嵌套测试提高组织性
@Nested
@DisplayName("特殊场景测试")
class SpecialCasesTest {
@Test
void testConcurrentAccess() throws InterruptedException {
// 多线程测试
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
new Calculator().calculateDiscount(100, 2, CustomerType.NORMAL);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
new Calculator().isPrime(i);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
// 3. 条件测试(根据环境条件)
@Test
@EnabledOnOs(OS.MAC)
void testOnlyOnMac() {
// 仅在Mac上运行
}
@Disabled("暂时禁用,正在修复")
void temporarilyDisabledTest() {
}
}
// 4. 使用Assumptions进行条件测试
@Test
void testWithAssumptions() {
// 检查系统属性
Assumptions.assumeTrue(System.getProperty("os.name").contains("Windows"));
// 检查环境变量
Assumptions.assumeFalse(System.getenv("CI") != null);
// 正常的测试代码
assertTrue(true);
}
// 5. 重复测试
@RepeatedTest(value = 3, name = "重复测试 {currentRepetition}/{totalRepetitions}")
void repeatedTest(RepetitionInfo repetitionInfo) {
// 重复运行测试
assertTrue(true);
}
}
覆盖率分析和改进建议
# jacoco-check.xml - JaCoCo检查配置
{
"rules": [
{
"element": "CLASS",
"limits": [
{
"counter": "INSTRUCTION",
"value": "COVEREDRATIO",
"minimum": "0.80"
},
{
"counter": "BRANCH",
"value": "COVEREDRATIO",
"minimum": "0.75"
}
]
}
],
"excludes": [
"**/dto/**",
"**/entity/**",
"**/config/**"
]
}
最佳实践清单
// BestPractices.java - 测试最佳实践示例
class BestPractices {
// ✅ 不要:测试和方法名不清晰
// ❌ public void test1()
// ✅ 使用规范的方法名
@Test
void shouldReturnDiscountedPriceWhenQuantityExceedsThreshold() {
// ...
}
// ✅ 遵循AAA模式(Arrange-Act-Assert)
@Test
void shouldApplyTenPercentDiscountForMultipleItems() {
// Arrange
Calculator calc = new Calculator();
double price = 100.0;
int quantity = 5;
// Act
double result = calc.calculateDiscount(price, quantity, CustomerType.NORMAL);
// Assert
assertThat(result).isEqualTo(450.0);
}
// ✅ 测试隔离性
@Test
void testsShouldBeIndependent() {
// 每个测试应该独立运行
// 不依赖其他测试的执行
}
}
覆盖率计算方式说明:
- 行覆盖率:被执行的代码行数/总代码行数
- 分支覆盖率:被执行的分支数/总分支数
- 方法覆盖率:被调用的方法数/总方法数
- 类覆盖率:被执行的类数/总类数
- 指令覆盖率:被执行的字节码指令数/总指令数
运行结果示例:
================ COVERAGE SUMMARY ================
Calendar.java: Line Coverage: 92.5%, Branch Coverage: 85.7%
Calculator.java: Line Coverage: 95.2%, Branch Coverage: 90.0%
--------------------------------------------------
Total: Line Coverage: 93.8%, Branch Coverage: 87.8%,
Method Coverage: 100%, Class Coverage: 100%
=================================================
BUILD SUCCESS
这个完整案例涵盖了Java单元测试覆盖率的各种场景和方法,能够帮助您系统性地提高代码质量。