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