單元測試的好處到底有哪些?每次單測啟動應(yīng)用,太耗時,怎么辦?二方三方接口可能存在日常沒法用,只能上預(yù)發(fā)/正式的情況,上預(yù)發(fā)測低效如何處理?本文分享三個單元測試神器及相關(guān)經(jīng)驗總結(jié)。
一 首先什么是好代碼?
Q1:好代碼應(yīng)具備可讀性,可測試性,可擴展性等等,那么如何寫出好代碼?
A:設(shè)計思想 & 編碼規(guī)范。
二 設(shè)計思想&設(shè)計原則&設(shè)計模式
1 設(shè)計原則(S.O.L.I.D)
SRP 單一職責(zé)原則
- 軟件模塊應(yīng)該只有一個被修改的理由。在大多數(shù)情況下,編寫Java代碼時都會將單一職責(zé)原則應(yīng)用于類。單一職責(zé)原則可被視為使封裝工作達到最佳狀態(tài)的良好實踐。更改的理由是:需要修改代碼。
- 單一原則,類、方法只干一件事。
OCP 開閉原則
- 模塊、類和函數(shù)應(yīng)該對擴展開放,對修改關(guān)閉。
- 通過繼承和多態(tài)擴展來添加新功能。開閉原則是最重要的設(shè)計原則之一,是大多數(shù)設(shè)計模式的基礎(chǔ)。
- 軟件建設(shè)一個復(fù)雜的結(jié)構(gòu),當(dāng)我們完成其中的一部分,就應(yīng)該不要修改它,而是在其基礎(chǔ)上繼續(xù)建設(shè)。
LSP 里式替換原則
- 在設(shè)計模塊和類時,必須確保派生類型從行為的角度來看是可替代的。
- 使用父類的地方都可以用子類替代。
- 父類最好為抽象類。
- 子類可實現(xiàn)父類的非抽象方法,盡量不要覆蓋重寫已實現(xiàn)的方法。
- 子類可寫自身的方法,有自身的特性,在父類的基礎(chǔ)上擴建。
- 子類覆蓋重寫父類方法時,方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松,后置條件(返回值)要更嚴格。
ISP 接口隔離原則
- 減少了代碼耦合,使軟件更健壯,更易于維護和擴展。
- 客戶端不應(yīng)該依賴它所不需要的接口。
DIP 依賴倒置原則
- 高級模塊不應(yīng)該依賴低級模塊,兩者都應(yīng)該依賴抽象。
- 抽象不應(yīng)該依賴于細節(jié),細節(jié)應(yīng)該依賴于抽象。
DRY 原則、KISS 原則、YAGNI 原則、LOD 法則
- DRY:不要干重復(fù)的事兒。
- KISS:不要干復(fù)雜的事兒,思從深而行從簡。
- YAGNI:不要干不需要的事兒,尺度把握尤為重要,超越尺度則會有過度設(shè)計之嫌。
- LOD:最小依賴。
設(shè)計模式
設(shè)計模式最重要的點還是在于解耦和復(fù)用,創(chuàng)建型模式將創(chuàng)建代碼與使用代碼解耦,結(jié)構(gòu)型模式是將功能代碼解耦,行為型模式將行為代碼解耦,最終達到高內(nèi)聚,松耦合的目標,設(shè)計模式體現(xiàn)了設(shè)計原則。
附:我們經(jīng)常說的“高內(nèi)聚 松耦合”究竟什么是高內(nèi)聚,什么是松耦合?
- 高內(nèi)聚:相近功能放在同一類中,相近功能往往會被同時修改,放到同一個類中在修改時,代碼更易維護(指導(dǎo)類本身的設(shè)計)
- 松耦合:類與類之間的依賴關(guān)系簡單清晰,一個類的代碼改動不會或者很少導(dǎo)致依賴類的代碼修改(指導(dǎo)類間依賴關(guān)系設(shè)計)
Q2: 那么如何驗證代碼是好代碼呢?
A: CR & 單測(下面進入正題^_^)
三 什么是單測?
單元測試(unit testing),指由開發(fā)人員對軟件中的最小可測試單元進行檢查和驗證。對于單元測試中單元的含義,一般來說,要根據(jù)實際情況去判定其具體含義,如C語言中單元指一個函數(shù),Java里單元指一個類,圖形化的軟件中可以指一個窗口或一個菜單等。總的來說,單元就是人為規(guī)定的最小的被測功能模塊。單元測試是在軟件開發(fā)過程中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其他部分相隔離的情況下進行測試。
來源:https://baike.baidu.com/item/單元測試
四 為什么要寫單測?
1 異(che)常(huo)場(xian)景(chang)
相信大家肯定遇到過以下幾種情況:
- 測試環(huán)境沒問題,線上怎么就不行。
- 所有異常捕獲,一切盡在掌控(你以為你以為的是你以為的)。
- 祖?zhèn)鞔a,改個小功能(只有上帝知道)。
- .....
要想故障出的少,還得單測好好搞。
2 優(yōu)點
提高代碼正確性
- 流程判讀符合預(yù)期,按照步驟運行,邏輯正確。
- 執(zhí)行結(jié)果符合預(yù)期,代碼執(zhí)行后,結(jié)果正確。
- 異常輸出符合預(yù)期,執(zhí)行異常或者錯誤,超越程序邊界,保護自身。
- 代碼質(zhì)量符合預(yù)期,效率,響應(yīng)時間,資源消耗等。
發(fā)現(xiàn)設(shè)計問題
- 代碼可測性差
- 方法封裝不合理
- 流程不合理
- 設(shè)計漏洞等
提升代碼可讀性
易寫單測的方法一定是簡單好理解的,可讀性是高的,反之難寫的單測代碼是復(fù)雜的,可讀性差的。
順便微重構(gòu)
如設(shè)計不合理可微重構(gòu),保證代碼的可讀性以及健壯性。
提升開發(fā)人員自信心
經(jīng)過單元測試,能讓程序員對自己的代碼質(zhì)量更有信心,對實現(xiàn)方式記憶更深。
啟動速度,提升效率
不用重復(fù)啟動Pandora容器,浪費大量時間在容器啟動上,方便邏輯驗證。
場景保存(多場景)
在HSF控制臺中只能保存一套參數(shù),而單測可保存多套參數(shù),覆蓋各個場景,多條分支,就是一個個測試用例。
CodeReview時作為重點CR的地方
好的單測可作為指導(dǎo)文檔,方便使用者使用及閱讀
寫起來,相信你會發(fā)現(xiàn)更多單測帶來的價值。
3 舉個小例子
改動前:OSS文件夾概念是通過文件名創(chuàng)建的,下面改動前的方法入?yún)⑹荈ile,該方法可以正常使用,但是在寫單測的時候,我發(fā)現(xiàn)使用文件有兩個成本:
- 必須要有默認文件。
- 要編寫獲取文件的路徑的方法。
坑:本地獲取的路徑與在容器獲取的路徑是不一致的,復(fù)雜度明顯增高。
/**
* 向阿里云的OSS存儲中存儲文件 (改動前)
*
* @param client OSS客戶端
* @param file 上傳文件
* @return String 唯一MD5數(shù)字簽名
*/
private static void uploadObject2Oss(OSS client, File file, String bucketName, String dirName) throws Exception {
InputStream is = new FileInputStream(file);
String fileName = file.getName();
Long fileSize = file.length();
//創(chuàng)建上傳Object的Metadata
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(is.available());
metadata.setCacheControl("no-cache");
metadata.setHeader("Pragma", "no-cache");
metadata.setContentEncoding("utf-8");
metadata.setContentType(getContentType(fileName));
metadata.setContentDisposition("filename/filesize=" + fileName + "/" + fileSize + "Byte.");
//上傳文件
client.putObject(bucketName, dirName + PublicConstant.DIAGONAL_CHARACTER + fileName, is, metadata);
}
改動后:將入?yún)ile修改為inputStream,這樣便可省去創(chuàng)建文件以及編寫獲取獲取文件路徑方法,同時還避免了獲取路徑的坑,一舉兩得,也通過單測找到了代碼設(shè)計不合理之處。
/**
* 向阿里云的OSS存儲中存儲文件(改動后)
*
* @param client OSS 上傳client
* @param bucketName bucketName
* @param dirName 目錄
* @param is 輸入流
* @param fileName 文件名
* @param fileSize 文件大小
* @throws Exception
*/
private void uploadObject2Oss(OSS client, String bucketName, String dirName, InputStream is, String fileName,
long fileSize) throws Exception {
//創(chuàng)建上傳Object的Metadata
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(is.available());
metadata.setCacheControl("no-cache");
metadata.setHeader("Pragma", "no-cache");
metadata.setContentEncoding("utf-8");
metadata.setContentType(getContentType(fileName));
metadata.setContentDisposition("filename/filesize=" + fileName + "/" + fileSize + "Byte.");
//上傳文件
client.putObject(bucketName, dirName + PublicConstant.DIAGONAL_CHARACTER + fileName, is, metadata);
}
4 還想再舉一個
以下這個方法先不說可讀性問題,單從編寫單測來驗證邏輯是否正確,在寫單測時需要:
- 構(gòu)造sourceInfos列表
- 構(gòu)造String數(shù)組
- 構(gòu)造map對象
- 構(gòu)造List
- 構(gòu)造User 對象
顯然這個方法是非常復(fù)雜的,但是邏輯就是得到一個指定長度列表。
/**
* 按比例混排結(jié)果 (改動前)
* @param sourceInfos 渠道配比信息
* @param resultMap 結(jié)果
* @param pageSize 總條數(shù)
* @param aliuid 用戶id
* @return 結(jié)果集
*/
private List<String> getResultList(List<String[]> sourceInfos, Map<String, List<String>> resultMap, int pageSize, User user) {
Map<String, Integer> sourceNumMap = new HashMap<>(sourceInfos.size());
sourceInfos.stream().forEach(s -> sourceNumMap.put(s[0], Integer.parseInt(s[1]) * pageSize / 100));
List<String> resultList = new ArrayList<>();
resultMap.forEach((s, strings) -> resultList.addAll(strings.stream().limit(sourceNumMap.get(s)).collect(
Collectors.toList())));
// 彌補條數(shù),防止數(shù)據(jù)量不足
if (resultList.size() < pageSize) {
compensate(resultList, pageSize, user.getAliuid());
}
return resultList;
}
改動后:將入?yún)⒏臑長ist sourceInfos, int pageSize, String aliuid,將String[]改為SourceInfo,提升代碼可讀性,否則無從得知s[0]表示什么,s[1]表示什么,在寫單測時需要:
- 構(gòu)造List列表
- 構(gòu)造SourceInfo對象
經(jīng)過改造,可測試性、可讀性均有提升,另外在這個例子中其實user對象只使用了aliuid,無需傳入整個對象,遵循KISS原則。
/**
* 按比例混排結(jié)果
* @param sourceInfos 渠道配比信息
* @param pageSize 條數(shù)
* @param aliuid 用戶id
* @return 結(jié)果集
*/
private List<String> getResultList(List<SourceInfo> sourceInfos, int pageSize, String aliuid) {
// 獲取結(jié)果集
List<String> resultList = sourceInfos.stream()
.flatMap(sourceInfo -> {
int needNum = (int)(sourceInfo.getSourceRatio() * pageSize / 100);
return listSource(sourceInfo.getSourceChannel(), needNum, aliuid).stream();
}).collect(Collectors.toList());
// 補償數(shù)據(jù)
compensate(resultList, pageSize, aliuid());
return resultList;
}
五 如何寫好單測?
1 工具
工欲善其事必先利其器,抗拒寫單測的其中最主要的一個原因就是沒有神器在手!
Fast-tester
每次啟動應(yīng)用動輒就是幾分鐘起,想要測試一個方法,上個廁所回來可能應(yīng)用還沒啟動,如此低效,怎么愿意去寫,fast_tester只需要啟動應(yīng)用一次(tip: 添加注解及測試方法需要重新啟動應(yīng)用),支持測試代碼熱更新,后續(xù)可隨意編寫測試方法,一個字“秀”!
使用方式:
(1)需要引入jar包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fast-tester</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
(2)在test的package下創(chuàng)建TestApplication
/**
* @author QZJ
* @date 2020-08-03
*/
@SpringBootApplication
public class TestApplication {
public static void main(String[] args){
PandoraBootstrap.run(args);
ConfigurableApplicationContext context = SpringApplication.run(TestApplication.class, args);
// 將ApplicationContext傳給FastTester
FastTester.run(context);
}
}
(3)編寫需要依賴pandora容器的case
/**
* tip:添加注解及方法需要重新啟動應(yīng)用
*
* @author QZJ
* @date 2020-08-03
*/
@Slf4j
public class BucketServiceTest {@Autowired
BucketService bucketService;@Test
public void testSaveBucketInfo() {
BucketRequest bucketRequest = new BucketRequest();
// 缺少參數(shù)
bucketRequest.setAccessKeyId("123");
bucketRequest.setAccessKeySecret("123");
bucketRequest.setBucketDomain("123");
bucketRequest.setEndpoint("123");
bucketRequest.setRegionId("123");
bucketRequest.setRoleArn("123");
bucketRequest.setRoleSessionName("123");
Result<Long> result = bucketService.saveBucketInfo(bucketRequest);
log.info("缺少參數(shù) result :{}", JSON.toJSONString(result));
// bucketName 重復(fù)
bucketRequest.setBucketName("video2sky");
result = bucketService.saveBucketInfo(bucketRequest);
log.info("bucketName 重復(fù) result :{}", JSON.toJSONString(result));
// 正例(執(zhí)行后,則bucketName已存在,需更換bucketName)
bucketRequest.setBucketName("12345");
result = bucketService.saveBucketInfo(bucketRequest);
log.info("正例 result :{}", JSON.toJSONString(result));
}
@Test
public void testCreateBucketFolder() {
BucketFolderRequest bucketFolderRequest = new BucketFolderRequest();
bucketFolderRequest.setFolderPath("/test");
bucketFolderRequest.setAppName("wudao");
bucketFolderRequest.setDescription("data");
bucketFolderRequest.setWriteTokenExpireTime(3600L);
Result<Long> result = bucketService.createBucketFolder(bucketFolderRequest);
log.info("缺少參數(shù) result :{}", JSON.toJSONString(result));
// 錯誤的bucketId
bucketFolderRequest.setBucketId(1L);
result = bucketService.createBucketFolder(bucketFolderRequest);
log.info("錯誤的bucketId result :{}", JSON.toJSONString(result));
// 異常的讀時間,讀寫時間不得超過2小時
bucketFolderRequest.setWriteTokenExpireTime(7300L);
result = bucketService.createBucketFolder(bucketFolderRequest);
log.info("異常的讀時間 result :{}", JSON.toJSONString(result));
// 重復(fù)的bucketFolder
bucketFolderRequest.setBucketId(11L);
bucketFolderRequest.setWriteTokenExpireTime(3500L);
result = bucketService.createBucketFolder(bucketFolderRequest);
log.info("重復(fù)的bucketFolder result :{}", JSON.toJSONString(result));
// 正例 (本地與服務(wù)器默認文件地址不一致,所以本地?zé)o法執(zhí)行成功,除非改地址,或者添加分支代碼)
bucketFolderRequest.setFolderPath("/test2");
result = bucketService.createBucketFolder(bucketFolderRequest);
log.info("正例 result :{}", JSON.toJSONString(result));
}
}
(4)啟動TestApplication,輸入對應(yīng)類名,選擇要執(zhí)行的相應(yīng)方法即可(切換測試類,直接重新輸入類路徑(包名+文件名)即可,原理還是反射)。
Tip:如果service注解失敗,檢查測試包的層級,例如:
Junit
JUnit是一個Java語言的單元測試框架, Junit測試是程序員測試,即所謂白盒測試,因為程序員知道被測試的軟件如何(How)完成功能和完成什么樣(What)的功能。繼承TestCase類,就可以用Junit進行自動測試。
來源:https://baike.baidu.com/item/白盒測試
使用方式:
(1)私有方法測試
/**
* 普通類測試,無需啟動容器
*
* @author QZJ
* @date 2020-08-05
*/
@Slf4j
public class OssServiceTest {
private OssServiceImpl ossService = new OssServiceImpl();@Test
public void testCreateOssFolder() {
try {
// 私有方法測試:方法一:用反射(推薦);方法二:修改類中方法屬性(不推薦)
Method method = OssServiceImpl.class.getDeclaredMethod("createOssFolder",
new Class[] {OSS.class, String.class, String.class});
method.setAccessible(true);
OSS client = new OSSClientBuilder().build("oss-cn-beijing.aliyuncs.com", "**",
"****");
Object obj = method.invoke(ossService, new Object[] {client, "***", "wudao/test3"});
Assert.assertEquals(true, obj);
} catch (Exception e) {
Assert.fail("testCreateOssFolder fail");
}
}
}
(2)相關(guān)測試注解如@Ignore使用,相關(guān)屬性如timeout測試接口性能、expected異常期望返回結(jié)果使用,測試全部測試方法等。
/**
* 普通工具類測試
* @author QZJ
* @date 2020-08-05
*/
@Slf4j
public class DateUtilTest {@Ignore // 忽略該方法執(zhí)行結(jié)果
@Test
public void testGetCurrentTime(){
String dateStr = DateUtil.getCurrentTime("yyyy-MM-dd HH:mm");
log.info("date:{}", dateStr);
Assert.assertEquals("2020-08-05 17:22", dateStr);
}// 方法超時時間設(shè)置以及期望執(zhí)行拋出的異常類型設(shè)置(錯誤的日期格式解析異常)
@Test(timeout = 110L, expected = ParseException.class)
public void testString2Date() throws ParseException{
Date date = DateUtil.string2Date("20202-02 02:02");
log.info("date:{}" , date);
//Thread.sleep(200L);
}@BeforeClass
public static void beforeClass() {
log.info("before class");
}@AfterClass
public static void afterClass() {
log.info("after class");
}@Before
public void before() {
log.info("before");
}@After
public void after() {
log.info("after");
}public static void main(String[] args) {
// 不需啟動容器的情況下使用,跑類中所有case
Result result = JUnitCore.runClasses(DateUtilTest.class);
result.getFailures().stream().forEach(f -> System.out.println(f.toString()));
log.info("result:{}", result.wasSuccessful());
}
}
詳細使用文檔見:
https://wiki.jikexueyuan.com/project/junit/environment-setup.html
Mockito
Mockito是一個針對Java的mocking框架,主要作用mock請求及返回值。
Mockito可以隔離類之間的相互依賴,做到真正的方法級別單測。
使用方式:
(1)需要引入jar包
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
(2)編寫測試代碼(例子)
需要測試的方法中調(diào)用了二方/三方接口,而接口無測試環(huán)境,為了測試方法邏輯,可以模擬接口返回結(jié)果(對原先代碼無侵入),達到應(yīng)用內(nèi)測試閉環(huán)。
tip:mock數(shù)據(jù)并非真正的返回值,需要注意返回的結(jié)果類型,字符串長度等,防止出現(xiàn)轉(zhuǎn)化,入庫字段超長等問題。
@Override
public ConsumeCodeResult consumeCode(String code) {
// 權(quán)益核銷
if (code.startsWith(BENEFIT_CENTER_CODE_HEADER) && BENEFIT_CENTER_CODE_LENGTH == code.length()) {
return consumeCodeFromCodeBenefitCenter(code);
}
// 碼商核銷
return consumeCodeFromCodeCenter(code);
}/**
* 從權(quán)益中心核銷電子憑證
*
* @param code 電子碼
* @return 核銷結(jié)果
*/
private ConsumeCodeResult consumeCodeFromCodeBenefitCenter(String code) {
// 參數(shù)構(gòu)造
BenefitUseDTO benefitUseDTO = new BenefitUseDTO();
benefitUseDTO.setCouponCode(code);
benefitUseDTO.getExtendFields().put("configId", benefitId);
benefitUseDTO.getExtendFields().put("type", BenefitTypeEnum.CODE_GENERAL.getType().toString());
AlispResult alispResult = benefitService.useBenefit(benefitUseDTO);
log.info("benefitUseDTO:{}, result:{}", benefitUseDTO, alispResult);
if (alispResult.isSuccess()) {
BenefitUseResult benefitUseResult = (BenefitUseResult)alispResult.getValue();
return new ConsumeCodeResult(benefitUseResult.getOutOrderId(),
String.valueOf(benefitUseResult.getConfigId()), benefitUseResult.getUseTime());
}
// 已使用
if (BizErrorCodeEnum.BENEFIT_RECORD_USED.name().equals(alispResult.getErrCodeName())) {
throw new BizException(StudentErrorEnum.VERIFICATION_CODE_REPEAT);
} else if (BizErrorCodeEnum.BENEFIT_RECORD_NOT_EXIST.name().equals(alispResult.getErrCodeName())
|| BizErrorCodeEnum.BENEFIT_RECORD_EXPIRED.name().equals(alispResult.getErrCodeName())) {
// 不存在或者過期
throw new BizException(StudentErrorEnum.VERIFICATION_CODE_INVALID);
} else {
// 其他異常
throw new BizException(StudentErrorEnum.VERIFICATION_CODE_CONSUME_FAILED);
}
}
@Test
public void mockConsume(){
BenefitService benefitService = Mockito.mock(BenefitService.class);
// 核銷成功鏈路
AlispResult alispResult = new AlispResult(true);
BenefitUseResult benefitUseResult = new BenefitUseResult();
benefitUseResult.setConfigId(1L);
benefitUseResult.setOutOrderId("lalala");
benefitUseResult.setUseTime(new Date());
alispResult.setValue(benefitUseResult);Mockito.when(benefitService.useBenefit(Mockito.any(BenefitUseDTO.class))).thenReturn(alispResult);
ConsumeCodeService consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");
ConsumeCodeResult consumeCodeResult = consumeCodeService.consumeCode("082712345678");
System.out.println(JSON.toJSONString(consumeCodeResult));alispResult = new AlispResult(false);
// 已核銷鏈路
alispResult.setErrCodeName("BENEFIT_RECORD_USED");
// 已過期鏈路
//alispResult.setErrCodeName("BENEFIT_RECORD_EXPIRED");
// 碼不存在鏈路
//alispResult.setErrCodeName("BENEFIT_RECORD_NOT_EXIST");
// 其他返回錯誤
//alispResult.setErrCodeName("LALALA");Mockito.when(benefitService.useBenefit(Mockito.any(BenefitUseDTO.class))).thenReturn(alispResult);
consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");
try {
consumeCodeService.consumeCode("082712345678");
} catch (Exception e) {
e.printStackTrace();
}// 核銷碼頭有誤
consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");
try {
consumeCodeService.consumeCode("081712345678");
} catch (Exception e) {
e.printStackTrace();
}
// 核銷碼長度有誤
consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");
try {
consumeCodeService.consumeCode("08271234567");
} catch (Exception e) {
e.printStackTrace();
}
}
Mockito的功能非常多,可以驗證行為,做測試樁,匹配參數(shù),驗證調(diào)用次數(shù)和執(zhí)行順序等等,在這不一一枚舉了,更多詳細使用可見文檔:
https://github.com/hehonghui/mockito-doc-zh
2 覆蓋率
覆蓋率是度量測試完整性的一個手段,是測試有效性的一個度量。
覆蓋率準則
- 函式覆蓋率(Function coverage):有呼叫到程式中的每一個函式(或副程式)嗎?
- 指令覆蓋率(Statement coverage):若用控制流圖(英語:control flow graph)表示程式,有執(zhí)行到控制流圖中的每一個節(jié)點嗎?
- 判斷覆蓋率(Decision coverage):(和分支覆蓋率不同)若用控制流圖表示程式,有執(zhí)行到控制流圖中的每一個邊嗎?例如控制結(jié)構(gòu)中所有IF指令都有執(zhí)行到邏輯運算式成立及不成立的情形嗎?
- 條件覆蓋率(Condition coverage):也稱為謂詞覆蓋(predicate coverage),每一個邏輯運算式中的每一個條件(無法再分解的邏輯運算式)是否都有執(zhí)行到成立及不成立的情形嗎?條件覆蓋率成立不表示判斷覆蓋率一定成立。
- 條件/判斷覆蓋率(Condition/decision coverage):需同時滿足判斷覆蓋率和條件覆蓋率。
場景總結(jié)
必要的
復(fù)雜的
重要的
不寫無用的
具體還需自己判斷,但是要避免過度自信。
覆蓋率要求
是否覆蓋率越高越好?回歸根本,我們寫單測的意義最重要的一點是為了保證代碼的正確性,如果我們把復(fù)雜的、重要的、必要的測試覆蓋到,即可保證應(yīng)用的正確性,例如set、get方法,完全沒有必要寫單測,不必為了追求覆蓋率而刻意寫單測,尺度這個東西,無論何時何事都是要有分寸的。躬身入局,寫起來,會慢慢找到節(jié)奏的。
3 思想
測試工具是神兵利器,設(shè)計原則是內(nèi)功心法,設(shè)計原則作為編寫代碼的指導(dǎo)思想,單元測試作為驗證代碼好壞的有效途徑,共同推動代碼演進。
6 最后
影響單測落地的原因:
- 團隊無單測習(xí)慣,個人是否follow
- 業(yè)務(wù)壓力大,覺得寫單測耗時
- 覺得可有可無
- 單測是一個程序員的自我修養(yǎng)