單元測試實施最佳方案(背景、實施、覆蓋率統計)

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.pswp.cn/web/44626.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/44626.shtml
英文地址,請注明出處:http://en.pswp.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;搜索和復制所保存的數據。 我們也可以將數據存儲在文件中&…

淺析Kafka Streams中KTable.aggregate()方法的使用

KTable.aggregate() 方法是 Apache Kafka Streams API 中用于對流數據進行狀態化聚合的核心方法之一。這個方法允許你根據一個鍵值&#xff08;通常是<K,V>類型&#xff09;的流數據&#xff0c;應用一個初始值和一個聚合函數&#xff0c;來累積和更新一個狀態&#xff0…

MSPM0G3507(三十六)——超聲波PID控制小車固定距離

效果圖&#xff1a; 波形圖軟件是VOFA&#xff0c;B站有教程 &#xff0c;雖然有缺點但是非常簡單。 視頻效果&#xff1a; PID控制距離 之前發過只有超聲波測距的代碼&#xff0c;MSPM0G3507&#xff08;三十二&#xff09;——超聲波模塊移植代碼-CSDN博客 SYSCFG配置&#…

Ubuntu下如何設置程序include搜索路徑及鏈接路徑

添加庫的include及lib路徑 linux下系統默認路徑為 /usr/include, /usr/local/include, gcc在編譯程序時會按照當前目錄路徑->系統默認路徑->系統環境變量的路徑方式去查找&#xff0c;所以當我們想調用的庫未安裝在系統默認路徑時&#xff0c;我們可以通過手動添加環境變…

數據壓縮的藝術:Kylin Cube設計中的自動壓縮特性

數據壓縮的藝術&#xff1a;Kylin Cube設計中的自動壓縮特性 在大數據的浩瀚宇宙中&#xff0c;Apache Kylin以其卓越的數據立方體&#xff08;Cube&#xff09;技術&#xff0c;為企業提供快速的多維數據分析能力。隨著數據量的不斷增長&#xff0c;存儲效率成為了一個關鍵問…

用友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;希望對后面的小伙伴有所幫助吧。 要求…

什么是計算機數據結構的字典

字典數據結構在計算機編程領域中是一個非常重要且常用的數據結構。它也被稱為關聯數組、哈希表或映射&#xff08;Map&#xff09;&#xff0c;在不同編程語言中有不同的實現和稱呼&#xff0c;但其核心概念和用途大致相同。 字典數據結構是一種鍵值對&#xff08;key-value p…

Linux 軟件工具安裝

Linux 軟件包管理器 yum 什么是軟件包 在Linux下安裝軟件&#xff0c; 一個通常的辦法是下載到程序的源代碼&#xff0c; 并進行編譯&#xff0c;得到可執行程序。 但是這樣太麻煩了&#xff0c; 于是有些人把一些常用的軟件提前編譯好&#xff0c;做成軟件包(可以理解成wind…

動態路由的基本概念

動態路由的基本概念 什么是動態路由&#xff1f; 網絡中的路由器彼此之間相互通信&#xff0c;傳遞各自的路由信息&#xff0c;利用收到的路由信息來更新和維護自己的路由表的過程。 基于某種路由協議實現&#xff08;6大協議&#xff09;。 動態路由的特點&#xff1a; 減…

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…

構建自定義Tensorflow鏡像時用到的鏈接地址整理

NVIDIA相關&#xff1a; NVIDIA CUDA鏡像的docker hub&#xff1a;https://hub.docker.com/r/nvidia/cuda/tags?page&page_size&ordering&name12.4.1NVIDIA 構建的Tensorflow鏡像包&#xff1a;https://docs.nvidia.com/deeplearning/frameworks/tensorflow-rele…

項目屬性的精粹:Gradle中配置項目屬性的全面指南

項目屬性的精粹&#xff1a;Gradle中配置項目屬性的全面指南 在構建自動化的宏偉藍圖中&#xff0c;Gradle以其靈活的項目屬性配置脫穎而出。項目屬性是構建過程中可配置的參數&#xff0c;它們可以控制構建行為、定義條件邏輯&#xff0c;甚至影響依賴解析。本文將深入探討如…

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;其關鍵在于拓撲&…