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

[复制链接]
发表于 2026-2-21 10:04:14 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
1. 什么是单元测试?


对于许多开辟职员来说,单元测试肯定不陌生 单元测试是白盒测试的一种情势,它的目的是测试软件的最小单元——函数、方法或类。单元测试的紧张目的是验证代码的精确性,以确保每个单元按照预期实行。单元测试通常由开辟职员来写,通过单元测试,开辟职员可以在代码开辟阶段赶早发现和修复错误,进步代码的质量和可维护性。
1.1 集成测试 != 单元测试

假如在付出体系有一个Service,有个付出预下单的方法,逻辑是先根据订单号查询数据库中是否存在付出单,再调营销体系接口查询优惠券信息,然后根据优惠券信息盘算实际付出金额,末了再调用付出通道预下单。(不消去明确逻辑细节,这里的重点是,这个方法须要许多外部依赖才华正常实行,数据库、中央件、外部体系等等)
伪代码如下:
  1. @Service
  2. public class PayService {
  3.     @Autowired
  4.     private OrderPayRecordMapper orderPayRecordMapper;
  5.        
  6.     @Autowired
  7.     private FeishuService feishuService;
  8.    
  9.     @DubboReference
  10.     private MarketingService marketingService;
  11.     public PrePayResponse prePay(PrePayRequest prePayRequest) {
  12.         PrePayResponse response = PrePayResponse.builder().orderNo(prePayRequest.getOrderNo()).build();
  13.         // 【查询数据库】校验订单支付记录是否存在
  14.         OrderPayRecord existedOrderPayRecord = orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo());
  15.         if (existedOrderPayRecord != null && !PayStatusEnum.PENDING.equals(existedOrderPayRecord.getStatus())) {
  16.             throw new BusinessException("5311991", "存在支付中订单,请勿重复支付");
  17.         }
  18.         // 【调用营销系统】查询优惠信息
  19.         CouponResponse coupon = marketingService.queryCoupon(CouponRequest.builder().orderNo(prePayRequest.getOrderNo()).build()).getData();
  20.         // 【写数据库】创建订单支付记录
  21.         OrderPayRecord newOrderPayRecord = OrderPayRecord.builder()
  22.                 .orderNo(prePayRequest.getOrderNo())
  23.                 .status(PayStatusEnum.PENDING)
  24.                 .amount(calcRealAmount(prePayRequest.getAmount(), coupon))
  25.                 .build();
  26.         orderPayRecordMapper.insert(newOrderPayRecord);
  27.         // 【调用支付通道】预下单
  28.         AlipayPrePayResponse alipayPrePayResponse = AlipayClient.prePay(
  29.                 AlipayPrePayRequest.builder().orderNo(prePayRequest.getOrderNo()).amount(newOrderPayRecord.getAmount()).build()
  30.         );
  31.         if (!"SUCCESS".equals(alipayPrePayResponse.getResult())) {
  32.             feishuService.sendMessage("通道预下单失败 orderNo:%s", prePayRequest.getOrderNo());
  33.             throw new BusinessException("5319997", "通道预下单失败");
  34.         }
  35.         response.setPayNo(alipayPrePayResponse.getPayNo());
  36.         return response;
  37.     }
  38.     /**
  39.      * 计算优惠后的金额
  40.      */
  41.     private Long calcRealAmount(Long originAmount, CouponResponse coupon) {
  42.         if (coupon != null && coupon.getDiscount() > 0 && originAmount > coupon.getDiscount()) {
  43.             return NumberUtils.max(0L, originAmount - coupon.getDiscount());
  44.         }
  45.         return originAmount;
  46.     }
  47. }
复制代码
针对上面这个付出预下单的方法,许多开辟职员大概风俗于像下面如许写“单元测试”,构造一下入参,然后调用被测方法,末了打印一下效果:
  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest(classes = Application.class)
  3. public class PayServiceTest {
  4.     @Autowired
  5.     private PayService payService;
  6.     @Test
  7.     public void test() {
  8.         PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("123").amount(100L).build();
  9.         PrePayResponse prePayResponse = payService.prePay(prePayRequest);
  10.         System.out.println(prePayResponse);
  11.     }
  12. }
复制代码
但这是单元测试吗?
在被测方法中,须要查询数据库(查询、生存数据),须要调用营销体系接口查询优惠券信息,须要调用付出通道预下单接口,假如内部体系是微服务调用,还要起注册中央…… 这个“单元”是不是有点大了?

运行单元测试的时间,由于会启动整个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依赖:
  1. <dependency>
  2.     <groupId>junit</groupId>
  3.     <artifactId>junit</artifactId>
  4.     <version>4.12</version>
  5.     <scope>test</scope>
  6. </dependency>
复制代码
用一个@Test注解就能界说一个方法为测试方法,用Assert举行断言:
  1. import org.junit.Assert;
  2. import org.junit.Test;
  3. public class JUnitTest {
  4.     @Test
  5.     public void testFact() {
  6.         int calcResult=Math.addExact(1,1);
  7.         Assert.assertEquals(2, calcResult);
  8.     }
  9. }
复制代码
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静态方法的时间须要利用):
  1. <dependency>
  2.     <groupId>org.mockito</groupId>
  3.     <artifactId>mockito-core</artifactId>
  4.     <version>4.5.1</version>
  5.     <scope>test</scope>
  6. </dependency>
  7. <dependency>
  8.     <groupId>org.mockito</groupId>
  9.     <artifactId>mockito-inline</artifactId>
  10.     <version>4.5.1</version>
  11.     <scope>test</scope>
  12. </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类写单元测试:
  1. @RunWith(MockitoJUnitRunner.class)
  2. public class PayServiceTest {
  3.     @InjectMocks
  4.     private PayService payService;
  5.     @Mock
  6.     private OrderPayRecordMapper orderPayRecordMapper;
  7.     @Mock
  8.     private MarketingService marketingService;
  9.     @Test
  10.     public void testPrePaySuccess() {
  11.         // 单元测试内容
  12.     }
  13. }
复制代码
(3)对mock对象的非静态方法插桩,好比:
假设当通过orderPayRecordMapper.getByOrderNo(String orderNo)查询订单号为80984234938472的付出订单时,返回null;
假设当通过marketingService.queryCoupon(CouponRequest request)查询优惠券时,返回null;
可以如许写:
  1. @RunWith(MockitoJUnitRunner.class)
  2. public class PayServiceTest {
  3.     @InjectMocks
  4.     private PayService payService;
  5.     @Mock
  6.     private OrderPayRecordMapper orderPayRecordMapper;
  7.     @Mock
  8.     private MarketingService marketingService;
  9.     @Test
  10.     public void testPrePaySuccess() {
  11.         PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("80984234938472").amount(100L).build();
  12.         // 插桩 假设是第一次下单,数据库中还没有相同订单号的支付记录(执行到orderPayRecordMapper.getByOrderNo时,不会真正查数据库,会直接返回null)
  13.         Mockito.when(orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo())).thenReturn(null);
  14.         // 插桩 假设没有优惠券(执行到marketingService.queryCoupon时,不会真正调营销系统接口,会直接返回null)
  15.         Mockito.when(marketingService.queryCoupon(Mockito.any())).thenReturn(Response.buildSuccess(null));
  16.         // 执行被测方法
  17.         PrePayResponse prePayResponse = payService.prePay(prePayRequest);
  18.         // 断言
  19.         Assert.assertNotNull(prePayResponse);
  20.         Assert.assertNotNull(prePayResponse.getPayNo());
  21.     }
  22. }
复制代码
(4)参数匹配,上面在对orderPayRecordMapper.getByOrderNo()举行插桩时,方法入参可以传真实的,也可以传恣意值,好比对marketingService.queryCoupon()举行插桩时,方法入参传的Mockito.any()体现参数为恣意值的时间都返回thenReturn()指定的效果。别的,尚有Mockito.any(Class type)、Mockito.anyString()、Mockito.anyLong()……
(5)上面例子中付出通道预下单接口是通过一个静态方法AlipayClient.prePay()来调用的,Mockito3.4.0之后支持对静态方法打桩(须要依赖mockito-inline):
  1. @Test
  2. public void testPrePaySuccess() {
  3.    PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("80984234938472").amount(100L).build();
  4.    Mockito.when(orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo())).thenReturn(null);
  5.    Mockito.when(marketingService.queryCoupon(Mockito.any())).thenReturn(Response.buildSuccess(null));
  6.    // 插桩 假设调用支付通道预下单接口返回成功
  7.    MockedStatic<AlipayClient> alipayClientMockedStatic = Mockito.mockStatic(AlipayClient.class);
  8.    alipayClientMockedStatic.when(() -> AlipayClient.prePay(Mockito.any())).thenReturn(
  9.        AlipayPrePayResponse.builder().payNo("123").result("SUCCESS").build()
  10.    );
  11.    // 执行被测方法
  12.    PrePayResponse prePayResponse = payService.prePay(prePayRequest);
  13.    // 断言
  14.    Assert.assertNotNull(prePayResponse);
  15.    Assert.assertNotNull(prePayResponse.getPayNo());
  16.    // 注意mock的静态对象使用完毕要调用close()来释放,或者用try-with-resources方式
  17.    alipayClientMockedStatic.close();
  18. }
复制代码
注意为了包管测试隔离性、制止内存走漏,mock的静态对象利用完毕要调用close()来开释,大概用try-with-resources方式来开释,也可以在@Before中初始化(JUnit5中是@BeforeEach),在@After中开释(JUnit5中是@AfterEach):
  1. private MockedStatic<AlipayClient> alipayClientMockedStatic;
  2. @Before
  3. public void setUp() {
  4.         alipayClientMockedStatic = Mockito.mockStatic(AlipayClient.class);
  5. }
  6. @After
  7. public void tearDown() {
  8.         alipayClientMockedStatic.close();
  9. }
  10. @Test
  11. public void testPrePaySuccess() {
  12.     PrePayRequest prePayRequest = PrePayRequest.builder().orderNo("80984234938472").amount(100L).build();
  13.     Mockito.when(orderPayRecordMapper.getByOrderNo(prePayRequest.getOrderNo())).thenReturn(null);
  14.     Mockito.when(marketingService.queryCoupon(Mockito.any())).thenReturn(Response.buildSuccess(null));
  15.     // 插桩 假设调用支付通道预下单接口返回成功
  16.     alipayClientMockedStatic.when(() -> AlipayClient.prePay(Mockito.any())).thenReturn(
  17.         AlipayPrePayResponse.builder().payNo("123").result("SUCCESS").build()
  18.     );
  19.     // 执行被测方法
  20.     PrePayResponse prePayResponse = payService.prePay(prePayRequest);
  21.     // 断言
  22.     Assert.assertNotNull(prePayResponse);
  23.     Assert.assertNotNull(prePayResponse.getPayNo());
  24. }
复制代码
(6)验证方法实行,对于一些有返回值的方法,可以通过断言来举行预期判断,对于一些没有返回值的void方法,可以通过verify来验证这个方法是否实行(乐成),好比在上面例子中,验证feishuService.sendMessage()这个方法是否被乐成实行:
  1. Mockito.verify(feishuService).sendMessage(Mockito.any()); // 验证feishuService.sendMessage()方法成功执行了1次
  2. Mockito.verify(feishuService,Mockito.times(2)).sendMessage(Mockito.any()); // 验证feishuService.sendMessage()方法成功执行了2次
复制代码
(7)非常断言,当预期某个分支会抛非常时,可以通过如下方式:
① 通过自界说方式:
  1. @Test
  2. public void testPrePayTimeout{
  3.     try {
  4.         payService.prePay();
  5.         Assert.fail();
  6.     } catch (Exception e) {
  7.         Assert.assertTrue(e instanceof TimeoutException);
  8.         Assert.assertEquals("超时啦",e.getMessage());
  9.     }
  10. }
复制代码
② 通过Mockito的方式,当判断某个分支是否抛非常时,可以通过@Rule来界说非常断言,好比
  1. @Rule
  2. public ExpectedException thrown = ExpectedException.none();
  3. @Test
  4. public void testPrePayTimeout{
  5.     thrown.expect(TimeoutException.class); // 当执行payService.prepare()时,预期抛出TimeoutException异常
  6.     thrown.expectMessage("超时啦");         // 当执行payService.prepare()时,预期抛出异常message是"超时啦"
  7.     payService.prePay();
  8. }
复制代码
③ 通过JUnit的方式,假如只对指定的非常类做断言,JUnit中尚有一个比力简单的方式,直接在@Test注解上界说预期的非常:
  1. @Test(expected = TimeoutException.class)
  2. public void testPrePayTimeout{
  3.     payService.prePay();
  4. }
复制代码
(8)private方法怎样测试?一样平常private方法不发起举行单元测试,可以在测public方法的时间来测。固然也可以通过spring-test测试私有方法,通过ReflectionTestUtils.invokeMethod调用被测方法:
  1. 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文件中添加下面两个插件:
  1. <plugin>
  2.     <groupId>org.apache.maven.plugins</groupId>
  3.     <artifactId>maven-surefire-plugin</artifactId>
  4.     <version>2.18.1</version>
  5.     <configuration>
  6.         <skipTests>false</skipTests>
  7.         <testFailureIgnore>true</testFailureIgnore>
  8.         <argLine>${jacocoArgLine}</argLine>
  9.     </configuration>
  10. </plugin>
  11. <plugin>
  12.     <groupId>org.jacoco</groupId>
  13.     <artifactId>jacoco-maven-plugin</artifactId>
  14.     <version>0.8.6</version>
  15.     <executions>
  16.         <execution>
  17.             <goals>
  18.                 <goal>prepare-agent</goal>
  19.             </goals>
  20.             <configuration>
  21.                 <propertyName>jacocoArgLine</propertyName>
  22.             </configuration>
  23.         </execution>
  24.         <execution>
  25.             <id>report</id>
  26.             <phase>test</phase>
  27.             <goals>
  28.                 <goal>report</goal>
  29.             </goals>
  30.         </execution>
  31.     </executions>
  32. </plugin>
复制代码
假如是maven父子项目,可以在父项目添加上面两个插件(可以检测全部子项目代码的覆盖率),jacoco只能针对每个maven子项目天生单独的覆盖率陈诉,假如想要把天生的陈诉聚合在一起,可以找一个maven子模块来做陈诉聚合(好比我们可以让***-starter子项目来做陈诉聚合),须要包管两点:1 做陈诉聚合的模块须要添加对应陈诉模块的maven依赖;2 在***-starter子项目的pom文件中添加如下插件(还可以通过exclude标签来克制对某个包、类等天生单测覆盖率):
  1. <plugin>
  2.     <groupId>org.jacoco</groupId>
  3.     <artifactId>jacoco-maven-plugin</artifactId>
  4.     <version>0.8.6</version>
  5.     <configuration>
  6.         <excludes>
  7.             <exclude>
  8.                 **/com/danny/test/mapper/**
  9.             </exclude>
  10.         </excludes>
  11.     </configuration>
  12.     <executions>
  13.         <execution>
  14.             <id>my-report</id>
  15.             <phase>test</phase>
  16.             <goals>
  17.                 <goal>report-aggregate</goal>
  18.             </goals>
  19.             <configuration>
  20.                 <excludes>
  21.                     <exclude></exclude>
  22.                 </excludes>
  23.             </configuration>
  24.         </execution>
  25.     </executions>
  26. </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%的覆盖率,开辟职员大概会编写大量低质量、仅为了覆盖率的测试。这些测试不但无法进步代码质量,还大概增长维护负担,低落开辟服从。
  • 时间和本钱:编写和维护高覆盖率的单元测试须要大量的时间和本钱。在实际项目中,须要衡量项目进度和代码质量,公道分配资源,而不是一味寻求高覆盖率。
末了:单元测试覆盖率只能代表你测试过哪些代码,不能代表你是否测试好这些代码!不能盲目寻求代码覆盖率,而应该想办法计划更有效的案单测用例!

转载请注明出处《单元测试实验最佳方案(配景、实验、覆盖率统计) 》
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表