马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
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%的覆盖率,开辟职员大概会编写大量低质量、仅为了覆盖率的测试。这些测试不但无法进步代码质量,还大概增长维护负担,低落开辟服从。
- 时间和本钱:编写和维护高覆盖率的单元测试须要大量的时间和本钱。在实际项目中,须要衡量项目进度和代码质量,公道分配资源,而不是一味寻求高覆盖率。
末了:单元测试覆盖率只能代表你测试过哪些代码,不能代表你是否测试好这些代码!不能盲目寻求代码覆盖率,而应该想办法计划更有效的案单测用例!
转载请注明出处《单元测试实验最佳方案(配景、实验、覆盖率统计) 》
|