单元测试实施最佳方案(背景、实施、覆盖率统计)

news2024/12/25 13:59:38

1. 什么是单元测试?


对于很多开发人员来说,单元测试一定不陌生

单元测试是白盒测试的一种形式,它的目标是测试软件的最小单元——函数、方法或类。单元测试的主要目的是验证代码的正确性,以确保每个单元按照预期执行。单元测试通常由开发人员来写,通过单元测试,开发人员可以在代码开发阶段及早发现和修复错误,提高代码的质量和可维护性。

1.1 集成测试 != 单元测试

假如在支付系统有一个Service,有个支付预下单的方法,逻辑是先根据订单号查询数据库中是否存在支付单,再调营销系统接口查询优惠券信息,然后根据优惠券信息计算实际支付金额,最后再调用支付通道预下单。(不用去理解逻辑细节,这里的重点是,这个方法需要很多外部依赖才能正常执行,数据库、中间件、外部系统等等)

伪代码如下:

@Service
public class PayService {

    @Autowired
    private OrderPayRecordMapper orderPayRecordMapper;
	
    @Autowired
    private FeishuService feishuService;
    
    @DubboReference
    private MarketingService marketingService;

    public PrePayResponse prePay(PrePayRequest prePayRequest) {
        PrePayResponse response = PrePayResponse.builder().orderNo(prePayRequest.getOrderNo()).build();
        // 【查询数据库】校验订单支付记录是否存在
        OrderPayRecord existedOrderPayRecord = orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo());
        if (existedOrderPayRecord != null && !PayStatusEnum.PENDING.equals(existedOrderPayRecord.getStatus())) {
            throw new BusinessException("5311991", "存在支付中订单,请勿重复支付");
        }
        // 【调用营销系统】查询优惠信息
        CouponResponse coupon = marketingService.queryCoupon(CouponRequest.builder().orderNo(prePayRequest.getOrderNo()).build()).getData();
        // 【写数据库】创建订单支付记录
        OrderPayRecord newOrderPayRecord = OrderPayRecord.builder()
                .orderNo(prePayRequest.getOrderNo())
                .status(PayStatusEnum.PENDING)
                .amount(calcRealAmount(prePayRequest.getAmount(), coupon))
                .build();
        orderPayRecordMapper.insert(newOrderPayRecord);
        // 【调用支付通道】预下单
        AlipayPrePayResponse alipayPrePayResponse = AlipayClient.prePay(
                AlipayPrePayRequest.builder().orderNo(prePayRequest.getOrderNo()).amount(newOrderPayRecord.getAmount()).build()
        );
        if (!"SUCCESS".equals(alipayPrePayResponse.getResult())) {
            feishuService.sendMessage("通道预下单失败 orderNo:%s", prePayRequest.getOrderNo());
            throw new BusinessException("5319997", "通道预下单失败");
        }
        response.setPayNo(alipayPrePayResponse.getPayNo());
        return response;
    }

    /**
     * 计算优惠后的金额
     */
    private Long calcRealAmount(Long originAmount, CouponResponse coupon) {
        if (coupon != null && coupon.getDiscount() > 0 && originAmount > coupon.getDiscount()) {
            return NumberUtils.max(0L, originAmount - coupon.getDiscount());
        }
        return originAmount;
    }
}

针对上面这个支付预下单的方法,很多开发人员可能习惯于像下面这样写“单元测试”,构造一下入参,然后调用被测方法,最后打印一下结果:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class PayServiceTest {

    @Autowired
    private PayService payService;

    @Test
    public void test() {
        PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("123").amount(100L).build();
        PrePayResponse prePayResponse = payService.prePay(prePayRequest);
        System.out.println(prePayResponse);
    }
}

但这是单元测试吗?

在被测方法中,需要查询数据库(查询、保存数据),需要调用营销系统接口查询优惠券信息,需要调用支付通道预下单接口,如果内部系统是微服务调用,还要起注册中心…… 这个“单元”是不是有点大了?

在这里插入图片描述

运行单元测试的时候,因为会启动整个Spring容器,连接配置中心、注册中心,连接数据库,初始化Redis配置等等,所以测试一个方法会很慢很慢;还可能因为数据库没连上,或者营销系统挂了,或者通道接口返回“FAIL”,单测运行就直接报错了;即使这些所依赖的环境都没问题,如果要测试有优惠券的情况,还要在营销系统中新增优惠券信息,等等。这些限制条件极大影响了测试效率。

真正的单元测试应当独立于外部环境,具有隔离性,应尽量避免其他类或系统的副作用影响。单元测试的目标是一小段代码,例如方法或类,应该只关注被测代码的核心逻辑。外部依赖关系(那些不容易构造的环境或需要灵活返回预期结果的依赖)应从单元测试中移除,改为由测试框架创建的 mock 对象来替换依赖对象。一个对象被 mock 后,在执行测试时不会调用其真实方法逻辑。例如,通过 Mockito 框架 mock 的对象,实际上是根据插桩,为真实对象创建了一个代理。运行单元测试时,调用的是代理对象的方法。

举例来说,如果对 OrderPayRecordMapper、MarketingService、AliPayClient 进行 mock,那么在执行 payService.prePay() 时,执行到这些 mock 对象的方法时,并不会真正去操作数据库、通过 RPC 调用远程服务、通过 HTTP 调用第三方通道,而是根据插桩返回预期的结果。

在这里插入图片描述

通过使用 Mock 对象,可以确保测试的独立性、确定性和高效性,从而更好地验证代码的正确性和可靠性。Mock 对象不仅提高了测试的执行速度,还保证了测试结果的一致性,使其能够在各种环境中重复执行。

2. 为什么要写单元测试?


验证代码正确性,简便地模拟各种场景。 单元测试能够验证代码的基本功能是否按预期工作。每个小的代码片段(如函数或方法)的逻辑是否正确无误。程序运行的 bug 往往出现在一些边界条件、异常情况下,比如网络超时等,在集成环境中模拟这些异常情况都比较困难,通过单元测试可以方便地模拟各种情况。

保证重构后代码的正确性。 重构是开发中的家常便饭,但每次改动都可能带来未知的问题。很多时候我们不敢修改(重构)老代码的原因,就是因为不知道影响范围,担心影响其他逻辑。有了完善的单元测试,重构之后运行一下单测就能迅速验证功能是否依旧正常,极大降低了引入新bug的风险。

阅读单元测试能帮助我们快速熟悉代码。 良好的单元测试,可以作为一个类/方法的“文档”,未来开发人员变更,通过一个方法的单元测试,可以知道指定输入对应的预期输出是什么,不需要深入的阅读代码,便能知道这个方法大概实现了什么功能,有哪些特殊情况需要考虑等等。

单元测试成本很低,有利于集成测试进行,提高效率。 编写单元测试虽然会花费大量精力,但是一旦完成了单元测试的工作,很多基础的bug将会被发现,并且修复这些bug的成本很低(比如开发阶段在本地及时发现、修复这些bug,不用等部署到dev/test等环境运行时遇到某个bug,还得在本地修改,再重新部署到dev/test环境复测……) 。

经过单元测试的对象(接口、函数等)可靠性会得到保证,在将来的系统集成中,可以极大减少在一些简单的bug上花费的时间(比如空指针异常、数组下标越界、代码执行分支和预期不符等),从而可以把精力放在系统交互和全局的功能实现相关的测试上。

Capers Jones 在《Applied Software Measurement : Global Analysis of Productivity and Quality》中有一张图比较形象地描述了在软件生命周期中,bug产生的概率、bug被发现的概率、bug被修复的成本之间的关系:

在这里插入图片描述

从这个图中可以发现,bug发现的越晚,修复它的成本就越高。在开发阶段是产生bug概率最高的时候,在开发阶段也是修复bug成本最低的时候,如果暂时抛开TDD不谈,单元测试是性价比最高的测试。

3. 编写单元测试


3.1 单元测试的范围是什么?

一般推荐优先对核心业务逻辑代码、有复杂计算(比如金融、支付业务比较重要的计算)、复用性代码(如比较重要的工具类)等进行单元测试。

3.2 什么时候编写单元测试?

  • 在编写代码之前
    测试驱动开发(TDD,Test-Driven Development)是一种开发方法,要求在编写实际代码之前先编写单元测试,优点是可以确保每一行代码都有相应的测试覆盖,从而提高代码质量和稳定性。

  • 在实现功能代码的同时
    如果没有采用 TDD 方法,可以在实现功能代码的同时编写单元测试。这种方法可以在开发过程中及时发现和修复代码中的问题。

  • 在修复 bug 之前
    在修复 bug 之前,先编写一个能重现该 bug 的单元测试,然后修复代码使该测试通过。这可以确保 bug 被修复,并且防止以后再出现同样的问题。

  • 在重构代码之前
    在重构代码之前,先编写单元测试来验证当前代码的行为,然后进行重构。这样可以确保重构不会引入新的错误,并且功能保持不变。

  • 在添加新功能之前
    在添加新功能之前,编写单元测试可以确保新功能的正确性,并且不会破坏现有功能。

3.3 通过JUnit和Mockito编写单元测试

JUnit是Java中最流行的测试框架,目前主流的Mock工具有Mockito、Spock、JMockit、PowerMock、EasyMock等, Mockito的语法简介,易上手,使用者众多,因此我们选择使用JUnit来写单元测试,使用Mockito来mock对象。

一般写单元测试的步骤为:构造被测方法入参 -> 对依赖插桩 -> 执行被测方法 -> 断言

JUnit基础用法

maven依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

用一个@Test注解就能定义一个方法为测试方法,用Assert进行断言:

import org.junit.Assert;
import org.junit.Test;

public class JUnitTest {
    @Test
    public void testFact() {
        int calcResult=Math.addExact(1,1);
        Assert.assertEquals(2, calcResult);
    }
}

JUnit大家很熟悉,这里不再赘述。更多JUnit的使用,比如Rule、Timeout,JUnit5中的参数化测试等等,可以参考官方文档,文档中有很多Demo供参考:

JUnit4: https://junit.org/junit4/ 或者 https://github.com/junit-team/junit4/wiki/

JUnit5: https://junit.org/junit5/docs/current/user-guide/

Mockito基础用法

这里只列举一下Mockito常见的用法,在项目中有其他场景可以参考Mockito官方文档https://javadoc.io/static/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html

maven依赖(spring-boot-starter-test已经包含mockito-core,如果已经引了spring-boot-starter-test就不需要再引mockito-core了,另外mockito-inline是在mock静态方法的时候需要使用):

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.5.1</version>
    <scope>test</scope>
</dependency>

(1)Mockito中的mock对象

在 Mockito 中,主要有三种对象类型,分别由 @InjectMocks、@Mock 和 @Spy 注解来定义:

  • @InjectMocks:用于被测试的类。Mockito 会通过反射创建这个类的实例(类似于 Spring 容器为 @Component 修饰的类创建实例)。如果该实例有依赖,Mockito 会自动将标记为 @Mock 或 @Spy 的对象注入到这个实例中。单元测试执行时,会真正执行这个实例的方法。
  • @Mock:用于需要被 mock 的依赖(类或接口)。Mockito 会通过字节码生成框架(ByteBuddy)为其创建代理对象。单元测试执行时,不会调用真正的方法,而是根据插桩返回预期的结果。
  • @Spy:用于部分模拟的对象。Spy 对象既可以调用真实对象的方法,也可以模拟其行为。默认情况下,调用的是实际对象的方法;当对其插桩后,调用的是模拟后的行为。

简而言之,用 @InjectMocks 来修饰被测试的类(只能是类,不能是接口),用 @Mock 或 @Spy 来修饰需要 mock 的对象(类或接口都行)。

(2)写单元测试时,就不能用 @RunWith(SpringRunner.class) 和 @SpringBootTest(classes = Application.class) 了,因为我们不需要真正初始化所依赖的对象,也就不需要加载Spring应用上下文。

在单元测试类上添加@RunWith(MockitoJUnitRunner.class)注解,用来初始化Mockito,自动注入mock对象等,比如要对上面PayService类写单元测试:

@RunWith(MockitoJUnitRunner.class)
public class PayServiceTest {

    @InjectMocks
    private PayService payService;

    @Mock
    private OrderPayRecordMapper orderPayRecordMapper;

    @Mock
    private MarketingService marketingService;

    @Test
    public void testPrePaySuccess() {
        // 单元测试内容
    }
}

(3)对mock对象的非静态方法插桩,比如:

假设当通过orderPayRecordMapper.getByOrderNo(String orderNo)查询订单号为80984234938472的支付订单时,返回null;

假设当通过marketingService.queryCoupon(CouponRequest request)查询优惠券时,返回null;

可以这样写:

@RunWith(MockitoJUnitRunner.class)
public class PayServiceTest {

    @InjectMocks
    private PayService payService;

    @Mock
    private OrderPayRecordMapper orderPayRecordMapper;

    @Mock
    private MarketingService marketingService;

    @Test
    public void testPrePaySuccess() {
        PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("80984234938472").amount(100L).build();
        // 插桩 假设是第一次下单,数据库中还没有相同订单号的支付记录(执行到orderPayRecordMapper.getByOrderNo时,不会真正查数据库,会直接返回null)
        Mockito.when(orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo())).thenReturn(null);
        // 插桩 假设没有优惠券(执行到marketingService.queryCoupon时,不会真正调营销系统接口,会直接返回null)
        Mockito.when(marketingService.queryCoupon(Mockito.any())).thenReturn(Response.buildSuccess(null));
        // 执行被测方法
        PrePayResponse prePayResponse = payService.prePay(prePayRequest);
        // 断言
        Assert.assertNotNull(prePayResponse);
        Assert.assertNotNull(prePayResponse.getPayNo());
    }
}

(4)参数匹配,上面在对orderPayRecordMapper.getByOrderNo()进行插桩时,方法入参可以传真实的,也可以传任意值,比如对marketingService.queryCoupon()进行插桩时,方法入参传的Mockito.any()表示参数为任意值的时候都返回thenReturn()指定的结果。此外,还有Mockito.any(Class type)、Mockito.anyString()、Mockito.anyLong()……

(5)上面例子中支付通道预下单接口是通过一个静态方法AlipayClient.prePay()来调用的,Mockito3.4.0之后支持对静态方法打桩(需要依赖mockito-inline):

@Test
public void testPrePaySuccess() {
   PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("80984234938472").amount(100L).build();
   Mockito.when(orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo())).thenReturn(null);
   Mockito.when(marketingService.queryCoupon(Mockito.any())).thenReturn(Response.buildSuccess(null));
   // 插桩 假设调用支付通道预下单接口返回成功
   MockedStatic<AlipayClient> alipayClientMockedStatic = Mockito.mockStatic(AlipayClient.class);
   alipayClientMockedStatic.when(() -> AlipayClient.prePay(Mockito.any())).thenReturn(
       AlipayPrePayResponse.builder().payNo("123").result("SUCCESS").build()
   );
   // 执行被测方法
   PrePayResponse prePayResponse = payService.prePay(prePayRequest);
   // 断言
   Assert.assertNotNull(prePayResponse);
   Assert.assertNotNull(prePayResponse.getPayNo());
   // 注意mock的静态对象使用完毕要调用close()来释放,或者用try-with-resources方式
   alipayClientMockedStatic.close();
}

注意为了保证测试隔离性、避免内存泄漏,mock的静态对象使用完毕要调用close()来释放,或者用try-with-resources方式来释放,也可以在@Before中初始化(JUnit5中是@BeforeEach),在@After中释放(JUnit5中是@AfterEach):

private MockedStatic<AlipayClient> alipayClientMockedStatic;

@Before
public void setUp() {
	alipayClientMockedStatic = Mockito.mockStatic(AlipayClient.class);
}

@After
public void tearDown() {
	alipayClientMockedStatic.close();
}

@Test
public void testPrePaySuccess() {
    PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("80984234938472").amount(100L).build();
    Mockito.when(orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo())).thenReturn(null);
    Mockito.when(marketingService.queryCoupon(Mockito.any())).thenReturn(Response.buildSuccess(null));
    // 插桩 假设调用支付通道预下单接口返回成功
    alipayClientMockedStatic.when(() -> AlipayClient.prePay(Mockito.any())).thenReturn(
        AlipayPrePayResponse.builder().payNo("123").result("SUCCESS").build()
    );
    // 执行被测方法
    PrePayResponse prePayResponse = payService.prePay(prePayRequest);
    // 断言
    Assert.assertNotNull(prePayResponse);
    Assert.assertNotNull(prePayResponse.getPayNo());
}

(6)验证方法执行,对于一些有返回值的方法,可以通过断言来进行预期判断,对于一些没有返回值的void方法,可以通过verify来验证这个方法是否执行(成功),比如在上面例子中,验证feishuService.sendMessage()这个方法是否被成功执行:

Mockito.verify(feishuService).sendMessage(Mockito.any()); // 验证feishuService.sendMessage()方法成功执行了1次
Mockito.verify(feishuService,Mockito.times(2)).sendMessage(Mockito.any()); // 验证feishuService.sendMessage()方法成功执行了2次

(7)异常断言,当预期某个分支会抛异常时,可以通过如下方式:

① 通过自定义方式:

@Test
public void testPrePayTimeout{
    try {
        payService.prePay();
        Assert.fail();
    } catch (Exception e) {
        Assert.assertTrue(e instanceof TimeoutException);
        Assert.assertEquals("超时啦",e.getMessage());
    }
}

② 通过Mockito的方式,当判定某个分支是否抛异常时,可以通过@Rule来定义异常断言,比如

@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testPrePayTimeout{
    thrown.expect(TimeoutException.class); // 当执行payService.prepare()时,预期抛出TimeoutException异常
    thrown.expectMessage("超时啦");         // 当执行payService.prepare()时,预期抛出异常message是"超时啦"
    payService.prePay();
}

③ 通过JUnit的方式,如果只对指定的异常类做断言,JUnit中还有一个比较简单的方式,直接在@Test注解上定义预期的异常:

@Test(expected = TimeoutException.class)
public void testPrePayTimeout{
    payService.prePay();
}

(8)private方法如何测试?一般private方法不建议进行单元测试,可以在测public方法的时候来测。当然也可以通过spring-test测试私有方法,通过ReflectionTestUtils.invokeMethod调用被测方法:

PrePayResponse prePayResponse= ReflectionTestUtils.invokeMethod(payService, "prePay", prePayRequest);

3.4 人工写单元测试太累?要学会站在巨人的肩膀上

一个项目中,能坚持写单元测试是一件很不容易的事情,可能开发人员没有写单元测试的习惯,或者由于赶业务而没有时间去写,或者是在项目后期为代码编写单元测试工作量巨大,觉得编写单元测试浪费时间,总之有很多理由导致坚持不下去。

所以可以借助一些工具来为我们自动生成单元测试,比如Idea中有一些专门用来生成单元测试的插件比如TestMe、Squaretest、JCode5等,也可以利用AI插件比如通义灵码来生成单元测试。具体用什么,哪个好用,看个人习惯。不够有些工具自动生成的单元测试,可能参数什么的不符合要求,或者运行不通过,需要重新调整一下才可以。

4. 单元测试覆盖率检测


测试的时候,我们常常关心,是否所有代码都测试到了,这个指标就叫做“代码覆盖率”(code coverage),代码覆盖率是一个非常重要的质量指标。它可以帮助我们了解代码中哪些部分被测试覆盖,哪些部分可能存在风险。通常我们关注的覆盖率有几个测量维度:

  • 类覆盖率:测试用例覆盖的类的百分比。
  • 方法覆盖率:测试用例覆盖的方法的百分比。
  • 行覆盖率:测试用例执行的代码行数占总行数的百分比。
  • 分支覆盖率:代码中每个条件分支(如 if-else 语句)被测试用例执行的情况。
  • 指令覆盖率:测试用例执行的字节码指令占总指令数的百分比。

jacoco是一款比较强大的单测覆盖率检测工具,Idea中已经集成了Jacoco单元测试覆盖率检测,也可以通过它的maven插件来检测,在Jenkins等持续集成平台上打包部署的时候也可以进行检测(原理也是执行maven插件)。

4.1 通过Idea中集成的jacoco检测单元测试覆盖率

在Idea右上Configuration -> Edit

在这里插入图片描述

Modify options -> Specify alternative coverage runner

在这里插入图片描述

然后在Code Coverage那就能选择JaCoCo了(默认是Idea):

在这里插入图片描述

配置好之后,运行覆盖率检测:

在这里插入图片描述

就能检测当前单元测试对被测代码的覆盖率了,在右边栏Coverage里就是单元测试覆盖率结果,有类覆盖率、方法覆盖率、行覆盖率、分支覆盖率,双击类名,可以看到代码左边有不同颜色的标识,默认绿色表示完全覆盖,黄色表示部分覆盖,红色表示未覆盖:

在这里插入图片描述

4.2 使用jacoco的maven插件进行单测覆盖率检测

如果是简单的maven项目,直接在pom文件中添加下面两个插件:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <skipTests>false</skipTests>
        <testFailureIgnore>true</testFailureIgnore>
        <argLine>${jacocoArgLine}</argLine>
    </configuration>
</plugin>
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.6</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
            <configuration>
                <propertyName>jacocoArgLine</propertyName>
            </configuration>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

如果是maven父子项目,可以在父项目添加上面两个插件(可以检测所有子项目代码的覆盖率),jacoco只能针对每个maven子项目生成单独的覆盖率报告,如果想要把生成的报告聚合在一起,可以找一个maven子模块来做报告聚合(比如我们可以让***-starter子项目来做报告聚合),需要保证两点:1 做报告聚合的模块需要添加对应报告模块的maven依赖;2 在***-starter子项目的pom文件中添加如下插件(还可以通过exclude标签来禁止对某个包、类等生成单测覆盖率):

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.6</version>
    <configuration>
        <excludes>
            <exclude>
                **/com/danny/test/mapper/**
            </exclude>
        </excludes>
    </configuration>
    <executions>
        <execution>
            <id>my-report</id>
            <phase>test</phase>
            <goals>
                <goal>report-aggregate</goal>
            </goals>
            <configuration>
                <excludes>
                    <exclude></exclude>
                </excludes>
            </configuration>
        </execution>
    </executions>
</plugin>

写完单元测试,执行 mvn clean test 后,maven单模块项目会在 target\site\jacoco、聚合项目会在 target\site\jacoco-aggregate 目录生成单元测试覆盖率报告,打开index.html就可以看到整个项目、某个包、类的单测覆盖率:

在这里插入图片描述

每个指标的含义:

  • Instructions:Java 字节指令的覆盖率
  • Branches:分支覆盖率
  • Cxty(Cyclomatic Complexity):圈复杂度,Jacoco 会为每一个非抽象方法计算圈复杂度,圈复杂度的值表示在一个方法里面所有可能路径的最小数目,简单的说就是为了覆盖所有路径,所需要执行单元测试数量,圈复杂度大说明程序代码可能质量低且难于测试和维护。
  • Lines: 行覆盖率,只要本行有一条指令被执行,则本行则被标记为被执行。
  • Methods: 方法覆盖率,任何非抽象的方法,只要有一条指令被执行,则该方法被计为被执行。
  • Classes: 类覆盖率,所有类,包括接口,只要其中有一个方法被执行,则标记为被执行(构造函数和静态初始化块也算作方法)。

点进去某个被检测的项目 -> 包 -> 类,可以看到代码中具体哪个方法、哪一行没有覆盖:

在这里插入图片描述

在最左边可以看到有不同颜色(红、黄、绿)的小钻石,每行代码还可能有不同颜色(红、黄、绿)的背景。

其中钻石代表分支覆盖情况:

  • 红色钻石:当前行所有的分支都没有被覆盖

  • 黄色钻石:当前行只有部分分支被覆盖(鼠标放上去可以查看详情)

  • 绿色钻石:当前行所有分支都被覆盖

背景代表指令覆盖情况:

  • 红色背景:当前行没有任何指令被执行

  • 黄色背景:当前行只有部分指令被执行,这里解释下

  • 绿色背景:当前行所有指令都被执行

通过单元测试覆盖率,能够清晰地了解到某个类、某个方法、某行代码、某个分支等是否被覆盖,从而能够促使开发人员更高效地完善单元测试。

那单元测试覆盖率达到多少才算合理呢?答案是并没有明确的要求,70%-80%的覆盖率已经足够优秀,能够有效发现和避免大多数问题。不需要一味追求100%的覆盖率。

然而,部分公司团队可能是为了保证代码的严谨性,或者是“领导要求”,对单元测试覆盖率要求很高(甚至要求达到100%),这种做法看似合理,但实际上并不可取,原因有:

  • 边际效应递减:在覆盖率达到一定水平后,继续增加覆盖率的边际效应会递减。换句话说,达到80%的覆盖率和达到100%的覆盖率所付出的努力和资源差别巨大,而带来的质量提升却有限。

  • 实际价值有限:为了达到100%的覆盖率,开发人员可能会编写大量低质量、仅为了覆盖率的测试。这些测试不仅无法提高代码质量,还可能增加维护负担,降低开发效率。

  • 时间和成本:编写和维护高覆盖率的单元测试需要大量的时间和成本。在实际项目中,需要权衡项目进度和代码质量,合理分配资源,而不是一味追求高覆盖率。

最后:单元测试覆盖率只能代表你测试过哪些代码,不能代表你是否测试好这些代码!不能盲目追求代码覆盖率,而应该想办法设计更有效的案单测用例!


转载请注明出处《单元测试实施最佳方案(背景、实施、覆盖率统计) 》 https://blog.csdn.net/huyuyang6688/article/details/140397135

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1924528.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql-联合查询

一.联合查询的概念 .对于unio查询,就是把多次查询的结果合并起来,形成一个新的查询果集。 SELECT 字段列表 FROM 表A... UNION[ALL] SELECT 字段列表 FROM 表B...&#xff0c; 二.将薪资低于5000的员工,和年龄大于50岁的员工全部查询出来 select * from emp where salary&…

Vulnhub靶场 | DC系列 - DC2

目录 环境搭建渗透测试 环境搭建 靶机镜像下载地址&#xff1a;https://vulnhub.com/entry/dc-2,311/需要将靶机和 kali 攻击机放在同一个局域网里&#xff1b;本实验kali 的 IP 地址&#xff1a;192.168.10.146。 渗透测试 使用 nmap 扫描 192.168.10.0/24 网段存活主机 …

window下安装go环境

一、go官网下载安装包 官网地址如下&#xff1a;https://golang.google.cn/dl/ 选择对应系统的安装包&#xff0c;这里是window系统&#xff0c;可以选择zip包&#xff0c;下载完解压就可以使用 二、配置环境变量 这里的截图配置以win11为例 我的文件解压目录是 D:\Software…

【进阶篇-Day9:JAVA中单列集合Collection、List、ArrayList、LinkedList的介绍】

目录 1、集合的介绍1.1 概念1.2 集合的分类 2、单列集合&#xff1a;Collection2.1 Collection的使用2.2 集合的通用遍历方式2.2.1 迭代器遍历&#xff1a;&#xff08;1&#xff09;例子&#xff1a;&#xff08;2&#xff09;迭代器遍历的原理&#xff1a;&#xff08;3&…

Halcon机器视觉15种缺陷检测案例_2不均匀表面刮伤检测

2&#xff1a; 不均匀表面刮伤检测 思路 1、获取图像 2、分割图像 3、处理区域 4、获取大&#xff0c;小缺陷 效果 原图 代码 *02 不均匀表面刮伤检测 dev_update_off () dev_close_window ()*****************第一步 获取图像******************* read_image (Image, 2.不…

集成excel工具:自定义导入回调监听器、自定义类型转换器、web中的读

文章目录 I 封装导入导出1.1 定义工具类1.2 自定义读回调监听器: 回调业务层处理导入数据1.3 定义文件导入上下文1.4 定义回调协议II 自定义转换器2.1 自定义枚举转换器2.2 日期转换器2.3 时间、日期、月份之间的互转2.4 LongConverterIII web中的读3.1 使用默认回调监听器3.2…

NAT地址转换+多出口智能选路,附加实验内容

本章主要讲&#xff1a;基于目标IP、双向地址的转换 注意&#xff1a;基于目标NAT进行转换 ---基于目标IP进行地址转换一般是应用在服务器端口映射&#xff1b; NAT的基础知识 1、服务器映射 服务器映射是基于目标端口进行转换&#xff0c;同时端口号也可以进行修改&…

AI算法14-套索回归算法Lasso Regression | LR

套索回归算法概述 套索回归算法简介 在统计学和机器学习中&#xff0c;套索回归是一种同时进行特征选择和正则化&#xff08;数学&#xff09;的回归分析方法&#xff0c;旨在增强统计模型的预测准确性和可解释性&#xff0c; 正则化是一种回归的形式&#xff0c;它将系数估…

接口基础知识2:http通信的组成

课程大纲 一、http协议 HTTP&#xff08;Hypertext Transfer Protocol&#xff0c;超文本传输协议&#xff09;是互联网中被使用最广的一种网络协议&#xff0c;用于客户端与服务器之间的通信。 HTTP协议定义了一系列的请求方法&#xff0c;例如 GET、POST、PUT、DELETE 等&…

一篇学通Axios

Axios 是一个基于 Promise 的 HTTP 客户端&#xff0c;用于浏览器和 node.js 环境。它提供了一种简单易用的方式来发送 HTTP 请求&#xff0c;并支持诸如请求和响应拦截、转换数据、取消请求以及自动转换 JSON 数据等功能。 Axios 名字的由来 Axios 的名字来源于希腊神话中的…

在Linux系统实现瑞芯微RK3588部署rknntoolkit2进行模型转换

一、首先要先安装一个虚拟的环境 安装Miniconda包 Miniconda的官网链接:Minidonda官网 下载好放在要操作的linux系统,我用的是远程服务器的linux系统,我放在whl这个文件夹里面,这个文件夹是我自己创建的 运行安装 安装的操作都是yes就可以了 检查是否安装成功,输入下面…

秋招突击——7/13——多线程编程(基础知识回顾+编程练习 )

文章目录 引言基础知识Synchronized关键字使用方式用于同步方法针对同步块的方法静态方法使用原理解析 Volatile使用方式实现原理 final关键字 编程练习&#xff08;synchronized就能实现&#xff09;双线程轮流打印1-100个人实现参考实现 三线程顺序打出1-100个人实现参考实现…

笔记 4 :linux 0.11 中继续分析 0 号进程创建一号进程的 fork () 函数

&#xff08;27&#xff09;本条目开始&#xff0c; 开始分析 copy_process () 函数&#xff0c;其又会调用别的函数&#xff0c;故先分析别的函数。 get_free_page &#xff08;&#xff09; &#xff1b; 先 介绍汇编指令 scasb &#xff1a; 以及 指令 sstosd &#xff1a;…

[USACO24OPEN] Smaller Averages G (单调性优化dp)

来源 题目 Bessie 有两个长度为 N的数组&#xff08;1≤N≤500&#xff09;。第一个数组的第 i 个元素为 ai​&#xff08;1≤ai​≤10^6&#xff09;&#xff0c;第二个数组的第 i个元素为bi​&#xff08;1≤bi​≤10^6&#xff09;。 Bessie 希望将两个数组均划分为若干非空…

大数据开发中的数据驱动决策:关键问题与实践指南

目录 决策前的准备工作1. 我已经掌握了哪些信息&#xff1f;2. 我们已经做出决定了吗&#xff1f;3. 我们需要哪些额外信息以及何时需要&#xff1f; 决策过程中的关键问题1. 我们需要做这个决定吗&#xff1f;2. 错误地做出这个决定的代价是什么&#xff1f; 决策后的反思1. 我…

主机安全-开源HIDS字节跳动Elkeid安装使用

目录 概述什么是HIDSHIDS与NIDS的区别EDR、XDR是啥&#xff1f; Elkeid架构Elkeid Agent && Agent centerElkeid DriverElkeid RASPElkeid HUBService DiscoveryManager安装数据采集规则&告警 参考 概述 什么是HIDS HIDS&#xff08; host-based intrusion detec…

CentOS7.X系统部署Zabbix6.0版本(可跟做)

文章目录 一、部署环境说明二、基本环境部署步骤1、环境初始化操作2、部署并配置Nginx3、部署并配置PHP4、测试NginxPHP环境5、部署并配置MariaDB 三、Zabbix-Server部署步骤1、编译安装Zabbix-Server2、导入Zabbix初始化库3、配置Zabbix前端UI4、启动Zabbix-Server5、WEB页面配…

基于 BERT+BILSTM 实现情感分析分类(附源码)

目录 一、数据集 二、数据清洗和划分 2.1 安装依赖 2.2 清洗和划分 三、下载 Bert 模型 四、训练和测试模型 本文主要基于 Bert 和 BiLSTM 实现情感分类&#xff0c;其中参考了多个博客&#xff0c;具体见参考链接。 源码已上传Gitee : bert-bilstm-in-Sentiment-classi…

基于JavaSpringBoot+Vue+uniapp微信小程序校园宿舍管理系统设计与实现

基于JavaSpringBootVueuniapp微信小程序实现校园宿舍管理系统 目录 第一章 绪论 1.1 研究背景 1.2 研究现状 1.3 研究内容 第二章 相关技术介绍 2.1 Java语言 2.2 HTML网页技术 2.3 MySQL数据库 2.4 Springboot 框架介绍 2.5 VueJS介绍 2.6 ElementUI介绍 第三章 系…

pytorch训练的时候 shm共享内存不足,导致训练停止

1.查看shm情况 df -h /dev/shm内存已经满了&#xff0c;因为之前训练多次训练意外停止到shm中的缓存不能及时被清理 2、手动清理shm 依然没被释放 3、查看关联的进程&#xff0c;一个一个kill lsof |grep deletedkill -9 46619 44618 44617 。。。。。4、搞定