Java单元测试覆盖率案例

wen java案例 2

本文目录导读:

Java单元测试覆盖率案例

  1. 覆盖率类型及工具
  2. 业务代码示例
  3. 完整的单元测试代码
  4. 覆盖率报告示例
  5. 运行测试并生成覆盖率报告
  6. 测试覆盖率高阶技巧
  7. 覆盖率分析和改进建议
  8. 最佳实践清单
  9. 覆盖率计算方式说明:
  10. 运行结果示例:

我将为您提供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() {
        // 每个测试应该独立运行
        // 不依赖其他测试的执行
    }
}

覆盖率计算方式说明:

  1. 行覆盖率:被执行的代码行数/总代码行数
  2. 分支覆盖率:被执行的分支数/总分支数
  3. 方法覆盖率:被调用的方法数/总方法数
  4. 类覆盖率:被执行的类数/总类数
  5. 指令覆盖率:被执行的字节码指令数/总指令数

运行结果示例:

================ 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单元测试覆盖率的各种场景和方法,能够帮助您系统性地提高代码质量。

抱歉,评论功能暂时关闭!