本文目录导读:

我将为您提供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单元测试覆盖率的各种场景和方法,能够帮助您系统性地提高代码质量。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播