如何寫好單元測(cè)試?
阿里妹導(dǎo)讀:?jiǎn)卧獪y(cè)試的好處到底有哪些?每次單測(cè)啟動(dòng)應(yīng)用,太耗時(shí),怎么辦?二方三方接口可能存在日常沒法用,只能上預(yù)發(fā)/正式的情況,上預(yù)發(fā)測(cè)低效如何處理?本文分享三個(gè)單元測(cè)試神器及相關(guān)經(jīng)驗(yàn)總結(jié)。
一 首先什么是好代碼?
Q1:好代碼應(yīng)具備可讀性,可測(cè)試性,可擴(kuò)展性等等,那么如何寫出好代碼?
A:設(shè)計(jì)思想 & 編碼規(guī)范。
二 設(shè)計(jì)思想&設(shè)計(jì)原則&設(shè)計(jì)模式
1 設(shè)計(jì)原則(S.O.L.I.D)
SRP 單一職責(zé)原則
- 軟件模塊應(yīng)該只有一個(gè)被修改的理由。在大多數(shù)情況下,編寫Java代碼時(shí)都會(huì)將單一職責(zé)原則應(yīng)用于類。單一職責(zé)原則可被視為使封裝工作達(dá)到最佳狀態(tài)的良好實(shí)踐。更改的理由是:需要修改代碼。
- 單一原則,類、方法只干一件事。
OCP 開閉原則
- 模塊、類和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。
- 通過繼承和多態(tài)擴(kuò)展來添加新功能。開閉原則是最重要的設(shè)計(jì)原則之一,是大多數(shù)設(shè)計(jì)模式的基礎(chǔ)。
- 軟件建設(shè)一個(gè)復(fù)雜的結(jié)構(gòu),當(dāng)我們完成其中的一部分,就應(yīng)該不要修改它,而是在其基礎(chǔ)上繼續(xù)建設(shè)。
LSP 里式替換原則
- 在設(shè)計(jì)模塊和類時(shí),必須確保派生類型從行為的角度來看是可替代的。
- 使用父類的地方都可以用子類替代。
- 父類最好為抽象類。
- 子類可實(shí)現(xiàn)父類的非抽象方法,盡量不要覆蓋重寫已實(shí)現(xiàn)的方法。
- 子類可寫自身的方法,有自身的特性,在父類的基礎(chǔ)上擴(kuò)建。
- 子類覆蓋重寫父類方法時(shí),方法的前置條件(即方法的形參)要比父類方法的輸入?yún)?shù)更寬松,后置條件(返回值)要更嚴(yán)格。
ISP 接口隔離原則
- 減少了代碼耦合,使軟件更健壯,更易于維護(hù)和擴(kuò)展。
- 客戶端不應(yīng)該依賴它所不需要的接口。
DIP 依賴倒置原則
- 高級(jí)模塊不應(yīng)該依賴低級(jí)模塊,兩者都應(yīng)該依賴抽象。
- 抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。
DRY 原則、KISS 原則、YAGNI 原則、LOD 法則
- DRY:不要干重復(fù)的事兒。
- KISS:不要干復(fù)雜的事兒,思從深而行從簡(jiǎn)。
- YAGNI:不要干不需要的事兒,尺度把握尤為重要,超越尺度則會(huì)有過度設(shè)計(jì)之嫌。
- LOD:最小依賴。
設(shè)計(jì)模式
設(shè)計(jì)模式最重要的點(diǎn)還是在于解耦和復(fù)用,創(chuàng)建型模式將創(chuàng)建代碼與使用代碼解耦,結(jié)構(gòu)型模式是將功能代碼解耦,行為型模式將行為代碼解耦,最終達(dá)到高內(nèi)聚,松耦合的目標(biāo),設(shè)計(jì)模式體現(xiàn)了設(shè)計(jì)原則。
附:我們經(jīng)常說的“高內(nèi)聚 松耦合”究竟什么是高內(nèi)聚,什么是松耦合?
- 高內(nèi)聚:相近功能放在同一類中,相近功能往往會(huì)被同時(shí)修改,放到同一個(gè)類中在修改時(shí),代碼更易維護(hù)(指導(dǎo)類本身的設(shè)計(jì))
- 松耦合:類與類之間的依賴關(guān)系簡(jiǎn)單清晰,一個(gè)類的代碼改動(dòng)不會(huì)或者很少導(dǎo)致依賴類的代碼修改(指導(dǎo)類間依賴關(guān)系設(shè)計(jì))
Q2: 那么如何驗(yàn)證代碼是好代碼呢?
A: CR & 單測(cè)(下面進(jìn)入正題^_^)
三 什么是單測(cè)?
單元測(cè)試(unit testing),指由開發(fā)人員對(duì)軟件中的最小可測(cè)試單元進(jìn)行檢查和驗(yàn)證。對(duì)于單元測(cè)試中單元的含義,一般來說,要根據(jù)實(shí)際情況去判定其具體含義,如C語言中單元指一個(gè)函數(shù),Java里單元指一個(gè)類,圖形化的軟件中可以指一個(gè)窗口或一個(gè)菜單等。總的來說,單元就是人為規(guī)定的最小的被測(cè)功能模塊。單元測(cè)試是在軟件開發(fā)過程中要進(jìn)行的最低級(jí)別的測(cè)試活動(dòng),軟件的獨(dú)立單元將在與程序的其他部分相隔離的情況下進(jìn)行測(cè)試。
來源:https://baike.baidu.com/item/單元測(cè)試
四 為什么要寫單測(cè)?
1 異(che)常(huo)場(chǎng)(xian)景(chang)
相信大家肯定遇到過以下幾種情況:
- 測(cè)試環(huán)境沒問題,線上怎么就不行。
- 所有異常捕獲,一切盡在掌控(你以為你以為的是你以為的)。
- 祖?zhèn)鞔a,改個(gè)小功能(只有上帝知道)。
- .....
要想故障出的少,還得單測(cè)好好搞。
2 優(yōu)點(diǎn)
提高代碼正確性
- 流程判讀符合預(yù)期,按照步驟運(yùn)行,邏輯正確。
- 執(zhí)行結(jié)果符合預(yù)期,代碼執(zhí)行后,結(jié)果正確。
- 異常輸出符合預(yù)期,執(zhí)行異?;蛘咤e(cuò)誤,超越程序邊界,保護(hù)自身。
- 代碼質(zhì)量符合預(yù)期,效率,響應(yīng)時(shí)間,資源消耗等。
發(fā)現(xiàn)設(shè)計(jì)問題
- 代碼可測(cè)性差
- 方法封裝不合理
- 流程不合理
- 設(shè)計(jì)漏洞等
提升代碼可讀性
- 易寫單測(cè)的方法一定是簡(jiǎn)單好理解的,可讀性是高的,反之難寫的單測(cè)代碼是復(fù)雜的,可讀性差的。
順便微重構(gòu)
- 如設(shè)計(jì)不合理可微重構(gòu),保證代碼的可讀性以及健壯性。
提升開發(fā)人員自信心
- 經(jīng)過單元測(cè)試,能讓程序員對(duì)自己的代碼質(zhì)量更有信心,對(duì)實(shí)現(xiàn)方式記憶更深。
啟動(dòng)速度,提升效率
不用重復(fù)啟動(dòng)Pandora容器,浪費(fèi)大量時(shí)間在容器啟動(dòng)上,方便邏輯驗(yàn)證。
場(chǎng)景保存(多場(chǎng)景)
- 在HSF控制臺(tái)中只能保存一套參數(shù),而單測(cè)可保存多套參數(shù),覆蓋各個(gè)場(chǎng)景,多條分支,就是一個(gè)個(gè)測(cè)試用例。
CodeReview時(shí)作為重點(diǎn)CR的地方
好的單測(cè)可作為指導(dǎo)文檔,方便使用者使用及閱讀
- 寫起來,相信你會(huì)發(fā)現(xiàn)更多單測(cè)帶來的價(jià)值。
3 舉個(gè)小例子
改動(dòng)前:OSS文件夾概念是通過文件名創(chuàng)建的,下面改動(dòng)前的方法入?yún)⑹荈ile,該方法可以正常使用,但是在寫單測(cè)的時(shí)候,我發(fā)現(xiàn)使用文件有兩個(gè)成本:
- 必須要有默認(rèn)文件。
- 要編寫獲取文件的路徑的方法。
坑:本地獲取的路徑與在容器獲取的路徑是不一致的,復(fù)雜度明顯增高。
- /**
- * 向阿里云的OSS存儲(chǔ)中存儲(chǔ)文件 (改動(dòng)前)
- *
- * @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);
- }
改動(dòng)后:將入?yún)ile修改為inputStream,這樣便可省去創(chuàng)建文件以及編寫獲取獲取文件路徑方法,同時(shí)還避免了獲取路徑的坑,一舉兩得,也通過單測(cè)找到了代碼設(shè)計(jì)不合理之處。
- /**
- * 向阿里云的OSS存儲(chǔ)中存儲(chǔ)文件(改動(dòng)后)
- *
- * @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è)
以下這個(gè)方法先不說可讀性問題,單從編寫單測(cè)來驗(yàn)證邏輯是否正確,在寫單測(cè)時(shí)需要:
- 構(gòu)造sourceInfos列表
- 構(gòu)造String數(shù)組
- 構(gòu)造map對(duì)象
- 構(gòu)造List
- 構(gòu)造User 對(duì)象
顯然這個(gè)方法是非常復(fù)雜的,但是邏輯就是得到一個(gè)指定長(zhǎng)度列表。
- /**
- * 按比例混排結(jié)果 (改動(dòng)前)
- * @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())));
- // 彌補(bǔ)條數(shù),防止數(shù)據(jù)量不足
- if (resultList.size() < pageSize) {
- compensate(resultList, pageSize, user.getAliuid());
- }
- return resultList;
- }
改動(dòng)后:將入?yún)⒏臑長(zhǎng)ist sourceInfos, int pageSize, String aliuid,將String[]改為SourceInfo,提升代碼可讀性,否則無從得知s[0]表示什么,s[1]表示什么,在寫單測(cè)時(shí)需要:
- 構(gòu)造List列表
- 構(gòu)造SourceInfo對(duì)象
經(jīng)過改造,可測(cè)試性、可讀性均有提升,另外在這個(gè)例子中其實(shí)user對(duì)象只使用了aliuid,無需傳入整個(gè)對(duì)象,遵循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());
- // 補(bǔ)償數(shù)據(jù)
- compensate(resultList, pageSize, aliuid());
- return resultList;
- }
五 如何寫好單測(cè)?
1 工具
工欲善其事必先利其器,抗拒寫單測(cè)的其中最主要的一個(gè)原因就是沒有神器在手!
Fast-tester
每次啟動(dòng)應(yīng)用動(dòng)輒就是幾分鐘起,想要測(cè)試一個(gè)方法,上個(gè)廁所回來可能應(yīng)用還沒啟動(dòng),如此低效,怎么愿意去寫,fast_tester只需要啟動(dòng)應(yīng)用一次(tip: 添加注解及測(cè)試方法需要重新啟動(dòng)應(yīng)用),支持測(cè)試代碼熱更新,后續(xù)可隨意編寫測(cè)試方法,一個(gè)字“秀”!
使用方式:
(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:添加注解及方法需要重新啟動(dòng)應(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));
- // 錯(cuò)誤的bucketId
- bucketFolderRequest.setBucketId(1L);
- result = bucketService.createBucketFolder(bucketFolderRequest);
- log.info("錯(cuò)誤的bucketId result :{}", JSON.toJSONString(result));
- // 異常的讀時(shí)間,讀寫時(shí)間不得超過2小時(shí)
- bucketFolderRequest.setWriteTokenExpireTime(7300L);
- result = bucketService.createBucketFolder(bucketFolderRequest);
- log.info("異常的讀時(shí)間 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ù)器默認(rèn)文件地址不一致,所以本地?zé)o法執(zhí)行成功,除非改地址,或者添加分支代碼)
- bucketFolderRequest.setFolderPath("/test2");
- result = bucketService.createBucketFolder(bucketFolderRequest);
- log.info("正例 result :{}", JSON.toJSONString(result));
- }
- }
(4)啟動(dòng)TestApplication,輸入對(duì)應(yīng)類名,選擇要執(zhí)行的相應(yīng)方法即可(切換測(cè)試類,直接重新輸入類路徑(包名+文件名)即可,原理還是反射)。
Tip:如果service注解失敗,檢查測(cè)試包的層級(jí),例如:
Junit
JUnit是一個(gè)Java語言的單元測(cè)試框架, Junit測(cè)試是程序員測(cè)試,即所謂白盒測(cè)試,因?yàn)槌绦騿T知道被測(cè)試的軟件如何(How)完成功能和完成什么樣(What)的功能。繼承TestCase類,就可以用Junit進(jìn)行自動(dòng)測(cè)試。
來源:https://baike.baidu.com/item/白盒測(cè)試
使用方式:
(1)私有方法測(cè)試
- /**
- * 普通類測(cè)試,無需啟動(dòng)容器
- *
- * @author QZJ
- * @date 2020-08-05
- */
- @Slf4j
- public class OssServiceTest {
- private OssServiceImpl ossService = new OssServiceImpl();
- @Test
- public void testCreateOssFolder() {
- try {
- // 私有方法測(cè)試:方法一:用反射(推薦);方法二:修改類中方法屬性(不推薦)
- 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)測(cè)試注解如@Ignore使用,相關(guān)屬性如timeout測(cè)試接口性能、expected異常期望返回結(jié)果使用,測(cè)試全部測(cè)試方法等。
- /**
- * 普通工具類測(cè)試
- * @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í)時(shí)間設(shè)置以及期望執(zhí)行拋出的異常類型設(shè)置(錯(cuò)誤的日期格式解析異常)
- @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) {
- // 不需啟動(dòng)容器的情況下使用,跑類中所有case
- Result result = JUnitCore.runClasses(DateUtilTest.class);
- result.getFailures().stream().forEach(f -> System.out.println(f.toString()));
- log.info("result:{}", result.wasSuccessful());
- }
- }
詳細(xì)使用文檔見:https://wiki.jikexueyuan.com/project/junit/environment-setup.html
Mockito
Mockito是一個(gè)針對(duì)Java的mocking框架,主要作用mock請(qǐng)求及返回值。
Mockito可以隔離類之間的相互依賴,做到真正的方法級(jí)別單測(cè)。
使用方式:
(1)需要引入jar包
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-all</artifactId>
- <version>1.9.5</version>
- <scope>test</scope>
- </dependency>
(2)編寫測(cè)試代碼(例子)
需要測(cè)試的方法中調(diào)用了二方/三方接口,而接口無測(cè)試環(huán)境,為了測(cè)試方法邏輯,可以模擬接口返回結(jié)果(對(duì)原先代碼無侵入),達(dá)到應(yīng)用內(nèi)測(cè)試閉環(huán)。
tip:mock數(shù)據(jù)并非真正的返回值,需要注意返回的結(jié)果類型,字符串長(zhǎng)度等,防止出現(xiàn)轉(zhuǎn)化,入庫(kù)字段超長(zhǎng)等問題。
- @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");
- // 其他返回錯(cuò)誤
- //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();
- }
- // 核銷碼長(zhǎng)度有誤
- consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");
- try {
- consumeCodeService.consumeCode("08271234567");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
Mockito的功能非常多,可以驗(yàn)證行為,做測(cè)試樁,匹配參數(shù),驗(yàn)證調(diào)用次數(shù)和執(zhí)行順序等等,在這不一一枚舉了,更多詳細(xì)使用可見文檔:https://github.com/hehonghui/mockito-doc-zh
2 覆蓋率
覆蓋率是度量測(cè)試完整性的一個(gè)手段,是測(cè)試有效性的一個(gè)度量。
覆蓋率準(zhǔn)則
- 函式覆蓋率(Function coverage):有呼叫到程式中的每一個(gè)函式(或副程式)嗎?
- 指令覆蓋率(Statement coverage):若用控制流圖(英語:control flow graph)表示程式,有執(zhí)行到控制流圖中的每一個(gè)節(jié)點(diǎn)嗎?
- 判斷覆蓋率(Decision coverage):(和分支覆蓋率不同)若用控制流圖表示程式,有執(zhí)行到控制流圖中的每一個(gè)邊嗎?例如控制結(jié)構(gòu)中所有IF指令都有執(zhí)行到邏輯運(yùn)算式成立及不成立的情形嗎?
- 條件覆蓋率(Condition coverage):也稱為謂詞覆蓋(predicate coverage),每一個(gè)邏輯運(yùn)算式中的每一個(gè)條件(無法再分解的邏輯運(yùn)算式)是否都有執(zhí)行到成立及不成立的情形嗎?條件覆蓋率成立不表示判斷覆蓋率一定成立。
- 條件/判斷覆蓋率(Condition/decision coverage):需同時(shí)滿足判斷覆蓋率和條件覆蓋率。
場(chǎng)景總結(jié)
- 必要的
- 復(fù)雜的
- 重要的
- 不寫無用的
具體還需自己判斷,但是要避免過度自信。
覆蓋率要求
是否覆蓋率越高越好?回歸根本,我們寫單測(cè)的意義最重要的一點(diǎn)是為了保證代碼的正確性,如果我們把復(fù)雜的、重要的、必要的測(cè)試覆蓋到,即可保證應(yīng)用的正確性,例如set、get方法,完全沒有必要寫單測(cè),不必為了追求覆蓋率而刻意寫單測(cè),尺度這個(gè)東西,無論何時(shí)何事都是要有分寸的。躬身入局,寫起來,會(huì)慢慢找到節(jié)奏的。
3 思想
測(cè)試工具是神兵利器,設(shè)計(jì)原則是內(nèi)功心法,設(shè)計(jì)原則作為編寫代碼的指導(dǎo)思想,單元測(cè)試作為驗(yàn)證代碼好壞的有效途徑,共同推動(dòng)代碼演進(jìn)。
6 最后
影響單測(cè)落地的原因:
- 團(tuán)隊(duì)無單測(cè)習(xí)慣,個(gè)人是否follow
- 業(yè)務(wù)壓力大,覺得寫單測(cè)耗時(shí)
- 覺得可有可無
- 單測(cè)是一個(gè)程序員的自我修養(yǎng)
【本文為51CTO專欄作者“阿里巴巴官方技術(shù)”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】