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

1. 什么是单元测试?


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

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

1.1 集成测试 != 单元测试

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

伪代码如下:

@Service
public class PayService {@Autowiredprivate OrderPayRecordMapper orderPayRecordMapper;@Autowiredprivate FeishuService feishuService;@DubboReferenceprivate 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 {@Autowiredprivate PayService payService;@Testpublic 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 {@Testpublic 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 {@InjectMocksprivate PayService payService;@Mockprivate OrderPayRecordMapper orderPayRecordMapper;@Mockprivate MarketingService marketingService;@Testpublic void testPrePaySuccess() {// 单元测试内容}
}

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

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

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

可以这样写:

@RunWith(MockitoJUnitRunner.class)
public class PayServiceTest {@InjectMocksprivate PayService payService;@Mockprivate OrderPayRecordMapper orderPayRecordMapper;@Mockprivate MarketingService marketingService;@Testpublic 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.rhkb.cn/news/376378.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

构建高精度室内定位导航系统,从3DGIS到AI路径规划的全面解析

室内定位导航系统是一种利用多种技术实现室内精准定位和导航的智能系统&#xff0c;即便没有卫星信号&#xff0c;也能实现精准导航。维小帮室内定位导航系统是基于自研的地图引擎与先进定位技术&#xff0c;结合智能路径规划算法&#xff0c;解决了人们在大型复杂室内场所最后…

【Linux】多线程_3

文章目录 九、多线程3. C11中的多线程4. 线程的简单封装 未完待续 九、多线程 3. C11中的多线程 Linux中是根据多线程库来实现多线程的&#xff0c;C11也有自己的多线程&#xff0c;那它的多线程又是怎样的&#xff1f;我们来使用一些C11的多线程。 Makefile&#xff1a; te…

Unity基础调色

叭叭叭 最近&#xff08;*这两天&#xff09;因为想做一些Unity的调色问题&#xff0c;尝试原文翻译一下&#xff0c;其实直接原文更好&#xff01;&#xff01; Color Grading 参考了&#xff0c;某大牛的翻译&#xff0c;实在忍不住了&#xff0c;我是不知道为什么能翻译成…

Vivado 2020.1 HLS IP在BD模式无法生成问题

折腾了一周整整&#xff0c;记录一下&#xff0c;希望对大家有用。 各种找、各种操作&#xff0c;也问了FAE&#xff0c;都没搞定。 最后看到如下博文的方法3&#xff0c;管用。 vivado综合hls类ip核报错问题解决方案_vivado ip synth checkpoint mode-CSDN博客 报错描述 m…

论文翻译:Large Language Models for Education: A Survey and Outlook

https://arxiv.org/abs/2403.18105 目录 教育领域的大型语言模型&#xff1a;一项调查和展望摘要1. 引言2. 教育应用中的LLM2.1 概述2.2 学习辅助2.2.1 问题解决&#xff08;QS&#xff09; 2.2.2 错误纠正&#xff08;EC&#xff09;2.2.3 困惑助手&#xff08;CH&#xff09;…

Jenkins中Node节点与构建任务

目录 节点在 Jenkins 中的主要作用 1. 分布式构建 分布式处理 负载均衡 2. 提供不同的运行环境 多平台支持 特殊环境需求 3. 提高资源利用率 动态资源管理 云端集成 4. 提供隔离和安全性 任务隔离 权限控制 5. 提高可扩展性 横向扩展 高可用性 Jenkins 主服务…

【香菇带你学Linux】Linux环境下gcc编译安装【建议收藏】

文章目录 0. 前言1. 安装前准备工作1.1 创建weihu用户1.2 安装依赖包1.2.1 安装 GMP1.2.2 安装MPFR1.2.3 安装MPC 2. gcc10.0.1版本安装3. 报错解决3. 1. wget下载报错 4. 参考文档 0. 前言 gcc&#xff08;GNU Compiler Collection&#xff09;是GNU项目的一部分&#xff0c;…

excel 百分位函数 学习

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、函数说明PERCENTILE 函数PERCENTILE.inc 函数PERCENTILE.exc 函数QUARTILE.EXC 函数 二、使用步骤总结 前言 excel 百分位函数 Excel提供了几个函数用于…

ctfshow-web入门-php特性(web100-web103)is_numeric 函数绕过

目录 1、web100 2、web101 3、web102 4、web103 1、web100 提示&#xff1a;flag in class ctfshow&#xff0c;我们只需要构造输出 ctfshow 这个类即可。 代码分析&#xff1a; $v0is_numeric($v1) and is_numeric($v2) and is_numeric($v3); if($v0){ 虽然逻辑运算符的…

Spring Boot整合Druid:轻松实现SQL监控和数据库密码加密

文章目录 1 引言1.1 简介1.2 Druid的功能1.3 竞品对比 2 准备工作2.1 项目环境 3 集成Druid3.1 添加依赖3.2 配置Druid3.3 编写测试类测试3.4 访问控制台3.5 测试SQL监控3.6 数据库密码加密3.6.1 执行命令加密数据库密码3.6.2 配置参数3.6.3 测试 4 总结 1 引言 1.1 简介 Dru…

gfast前端UI:基于Vue3与vue-next-admin适配手机、平板、pc 的后台开源模板

摘要 随着现代软件开发的高效化需求&#xff0c;一个能够快速适应不同设备、简化开发过程的前端模板变得至关重要。gfast前端UI&#xff0c;基于Vue3.x和vue-next-admin&#xff0c;致力于提供这样一个解决方案。本文将深入探讨gfast前端UI的技术栈、设计原则以及它如何适配手机…

Neo4j:图数据库的革命性力量

Neo4j 首席技术官 prathle 撰写了一篇出色的博文&#xff0c;总结最近围绕 GraphRAG 的热议、我们从一年来帮助用户使用知识图谱 LLM 构建系统中学到的东西&#xff0c;以及我们认为该领域的发展方向。Neo4j一时间又大火起来&#xff0c;本文将带你快速入门这神奇的数据库。 前…

sentinel源码分析: dashboard与微服务的交互、pull模式持久化

文章目录 原始方式微服务端规则如何保存规则如何加载进内存微服务端接收控制台请求控制台推送规则总结 pull拉模式官方demo如何整合Spring Cloud整合Spring Cloud 前置知识 SentinelResource的实现原理、SphU.entry()方法中ProcessorSlotChain链、entry.exit() 建议先会使用se…

秋招Java后端开发冲刺——MyBatisPlus总结

一、 基本知识 1. 介绍 yBatis-Plus 是一个 MyBatis 的增强工具&#xff0c;在 MyBatis 的基础上增加了大量功能和简化操作&#xff0c;以提高开发效率。 2. 特点 无侵入&#xff1a;只做增强不做改变&#xff0c;引入它不会对现有项目产生影响。依赖少&#xff1a;仅仅依赖 …

谈谈软件交互设计

谈谈软件交互设计 交互设计的由来 交互设计(Interaction Design)这一概念,最初是由IDEO创始人之一Bill.Moggridge(莫格里奇)1984年在一次会议上提出。他设计了世界上第一台笔记本电脑Compass,并写作出版了在交互设计领域影响深远的《Designing Interactions》一书,被称…

mqtt.fx连接阿里云

本文主要是记述一下如何使用mqtt.fx连接在阿里云上创建好的MQTT服务。 1 根据MQTT填写对应端口即可 找到设备信息&#xff0c;里面有MQTT连接参数 2 使用物模型通信Topic&#xff0c;注意这里的post说设备上报&#xff0c;那也就是意味着云端订阅post&#xff1b;set则意味着设…

Linux内核编译安装 - Deepin,Debian系

为什么要自己编译内核 优点 定制化&#xff1a;你可以根据自己的硬件和需求配置内核&#xff0c;去掉不必要的模块&#xff0c;优化性能。性能优化&#xff1a;移除不需要的驱动程序和特性&#xff0c;减小内核体积&#xff0c;提高系统性能。最新特性和修复&#xff1a;获取…

【密码学】从有限状态自动机到密钥流生成器

本文是对流密码内容的拓展&#xff0c;在流密码中种子密钥通过一个伪随机数生成器产生一个与明文等长的伪随机密钥流。而本文的内容就是在回答这样两个问题&#xff1a; 伪随机密钥流是如何生成的&#xff1f;流密码、流密钥生成器和有限状态自动机之间是什么关系&#xff1f;…

Mac和VirtualBox Ubuntu共享文件夹

1、VirtualBox中点击设置->共享文件夹 2、设置共享文件夹路径和名称&#xff08;重点来了&#xff1a;共享文件夹名称&#xff09; 3、保存设置后重启虚拟机&#xff0c;执行下面的命令 sudo mkdir /mnt/share sudo mount -t vboxsf share /mnt/share/ 注&#xff1a;shar…

Gitea 仓库事件触发Jenkins远程构建

文章目录 引言I Gitea 仓库事件触发Jenkins远程构建1.1 Jenkins配置1.2 Gitea 配置引言 应用场景:项目部署 I Gitea 仓库事件触发Jenkins远程构建 Gitea支持用于仓库事件的Webhooks 1.1 Jenkins配置 高版本Jenkins需要关闭跨域限制和开启匿名用户访问 在Jenkins启动前加入…