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

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.mzph.cn/web/44626.shtml

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

相关文章

合肥高校大学智能制造实验室数字孪生可视化系统平台建设项目验收

合肥高校大学智能制造实验室近日迎来了一项重要时刻&#xff0c;数字孪生可视化系统平台建设项目顺利通过了验收。这一项目的成功实施&#xff0c;不仅标志着合肥高校在智能制造领域取得新的突破&#xff0c;为我国智能制造技术的发展注入新活力。 合肥高校智能制造实验室作为…

T972 切换至pdm 声音输入的方法

1.在hardware/amlogic/audio/audio_hal/audio_hw.c下&#xff0c;直接切换 在 static unsigned int select_port_by_device(struct aml_audio_device *adev) 中先强制切换为pdm 2.在device mk 配置文件中 #add fof fix the mic bug by jason 20230621 PRODUCT_PROPERTY_OVE…

MySQL 数据库基础概念

一、什么是数据库&#xff1f; 数据库&#xff08;Database&#xff09;是按照数据结构来组织、存储和管理数据的仓库。 每个数据库都有一个或多个不同的 API 用于创建&#xff0c;访问&#xff0c;管理&#xff0c;搜索和复制所保存的数据。 我们也可以将数据存储在文件中&…

MSPM0G3507(三十六)——超声波PID控制小车固定距离

效果图&#xff1a; 波形图软件是VOFA&#xff0c;B站有教程 &#xff0c;虽然有缺点但是非常简单。 视频效果&#xff1a; PID控制距离 之前发过只有超声波测距的代码&#xff0c;MSPM0G3507&#xff08;三十二&#xff09;——超声波模块移植代码-CSDN博客 SYSCFG配置&#…

用友NC Cloud blobRefClassSearch FastJson反序列化RCE漏洞复现

0x01 产品简介 用友 NC Cloud 是一种商业级的企业资源规划云平台,为企业提供全面的管理解决方案,包括财务管理、采购管理、销售管理、人力资源管理等功能,实现企业的数字化转型和业务流程优化。 0x02 漏洞概述 用友 NC Cloud blobRefClassSearch 接口处存在FastJson反序列…

开源PHP论坛HadSky本地部署与配置公网地址实现远程访问

文章目录 前言1. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3 Cpolar稳定隧道&#xff08;本地设置&#xff09;2.4 公网访问测试 总结 前言 今天和大家分享…

idea启动ssm项目详细教程

前言 今天碰到一个ssm的上古项目&#xff0c;项目没有使用内置的tomcat作为服务器容器&#xff0c;这个时候就需要自己单独设置tomcat容器。这让我想起了我刚入行时被外置tomcat配置支配的恐惧。现在我打算记录一下配置的过程&#xff0c;希望对后面的小伙伴有所帮助吧。 要求…

SpringBoot3.3.0升级方案

本文介绍了由SpringBoot2升级到SpringBoot3.3.0升级方案&#xff0c;新版本的升级可以解决旧版本存在的部分漏洞问题。 一、jdk17下载安装 1、下载 官网下载地址 Java Archive Downloads - Java SE 17 Jdk17下载后&#xff0c;可不设置系统变量java_home&#xff0c;仅在id…

开发技术-Java BigDecimal 精度丢失问题

文章目录 1. 背景2. 方法3. 总结 1. 背景 昨天和小伙伴排查一个问题时&#xff0c;发现一个 BigDecimal 精度丢失的问题&#xff0c;即 double a 1.1;BigDecimal ba new BigDecimal(a).subtract(new BigDecimal(0.1));System.out.println(ba);输出&#xff1a; 1.000000000…

Vue3 使用 Vue Router 时,prams 传参失效和报错问题

Discarded invalid param(s) “id“, “name“, “age“ when navigating 我尝试使用 prams 传递数据 <script setup> import { useRouter } from vue-routerconst router useRouter() const params { id: 1, name: ly, phone: 13246566476, age: 23 } const toDetail…

快速使用BRTR公式出具的大模型Prompt提示语

Role:文章模仿大师 Background: 你是一位文章模仿大师&#xff0c;擅长分析文章风格并进行模仿创作。老板常让你学习他人文章后进行模仿创作。 Attention: 请专注在文章模仿任务上&#xff0c;提供高质量的输出。 Profile: Author: 一博Version: 1.0Language: 中文Descri…

半边数据结构学习

半边数据结构学习 一、网格数据结构二、半边数据结构顶点(Vertex)半边(HalfEdge)面片(Face) 三、OpenMesh 相关代码拓扑关联对象遍历 四、OpenFilpper 相关代码HoleInfo类孔洞检测孔洞信息HoleFiller类孔洞补全 一、网格数据结构 对于表面网络来说&#xff0c;其关键在于拓扑&…

【MySQL系列】VARCHAR的底层存储

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

python-亲和数(赛氪OJ)

[题目描述] 古希腊数学家毕达哥拉斯在自然数研究中发现&#xff0c;220 的所有真约数(即不是自身的约数)之和为&#xff1a; 1245101120224455110&#xff1d;284 。 而 284 的所有真约为 1 、 2 、 4 、 71 、 142 &#xff0c;加起来恰好为 220 。人们对这样的数感到很惊奇&a…

如何搞定美国TikTok直播网络?

在全球范围内&#xff0c;TikTok已经积累了超过30亿次的下载量&#xff0c;月活跃用户达到13亿以上&#xff0c;支持75种语言&#xff0c;覆盖了150多个国家和地区。这一庞大的流量池吸引了众多国内电商人尝试在TikTok上进行业务拓展。本文将探讨如果要在美国运营TikTok直播&am…

MySql性能调优03-[SQL优化]

SQL优化 MySQL优化SQL优化-不要写select *SQL优化-小表驱动大表&#xff0c;而不是大表驱动小表SQL优化-连接查询代替子查询SQL优化-提升group by的效率SQL优化-使用limitSQL优化-union all代替unionSQL优化-join的表不宜过多 MySQL优化 trace工具 set session optimizer_trac…

Kithara与OpenCV (一)

Kithara使用 OpenCV 库 目录 Kithara使用 OpenCV 库简介需求和支持的环境构建 OpenCV 库使用 CMake 进行配置以与 Kithara 一起工作 使用 OpenCV 库设置项目运行 OpenCV 代码图像采集和 OpenCV自动并行化限制和局限性1.系统建议2.实时限制3.不支持的功能和缺失的功能4.显示 Ope…

数字化打造行业生态产业链,企业新增益全知道

在当今数字化时代&#xff0c;利用数字化打造行业生态产业链成为企业发展的重要战略选择。那么&#xff0c;这一举措究竟能为企业带来哪些新增益呢&#xff1f;让我们一探究竟。 一、运营效率大幅提高 数字化技术就像一条神奇的纽带&#xff0c;将产业链上的各个环节紧紧相…

Python函数 之 匿名函数

1.概念 匿名函数: 使用 lambda 关键字 定义的表达式&#xff0c;称为匿名函数. 2.语法 lambda 参数, 参数: 一行代码 # 只能实现简单的功能&#xff0c;只能写一行代码 # 匿名函数 一般不直接调用&#xff0c;作为函数的参数使用的 3.代码 4.练习 # 1, 定义匿名函数, 参数…

32路串口服务器 应用领域

32路串口服务器在多个领域有着广泛的应用&#xff0c;以下是详细的应用实例&#xff1a; 一、工业自动化 在工业自动化领域&#xff0c;32路串口服务器发挥着举足轻重的作用。传统的工业设备往往采用串口通信方式&#xff0c;而串口服务器能够将这些设备接入网络&#xff0c;…