如何寫好單元測試?

2021-03-02 阿里技術

阿里妹導讀:單元測試的好處到底有哪些?每次單測啟動應用,太耗時,怎麼辦?二方三方接口可能存在日常沒法用,只能上預發/正式的情況,上預發測低效如何處理?本文分享三個單元測試神器及相關經驗總結。

文末福利:《Linux運維學習路線》技術公開課。

Q1:好代碼應具備可讀性,可測試性,可擴展性等等,那麼如何寫出好代碼?DRY 原則、KISS 原則、YAGNI 原則、LOD 法則設計模式最重要的點還是在於解耦和復用,創建型模式將創建代碼與使用代碼解耦,結構型模式是將功能代碼解耦,行為型模式將行為代碼解耦,最終達到高內聚,鬆耦合的目標,設計模式體現了設計原則。附:我們經常說的「高內聚 鬆耦合」究竟什麼是高內聚,什麼是鬆耦合?單元測試(unit testing),指由開發人員對軟體中的最小可測試單元進行檢查和驗證。對於單元測試中單元的含義,一般來說,要根據實際情況去判定其具體含義,如C語言中單元指一個函數,Java裡單元指一個類,圖形化的軟體中可以指一個窗口或一個菜單等。總的來說,單元就是人為規定的最小的被測功能模塊。單元測試是在軟體開發過程中要進行的最低級別的測試活動,軟體的獨立單元將在與程序的其他部分相隔離的情況下進行測試。
來源:https://baike.baidu.com/item/單元測試1  異(che)常(huo)場(xian)景(chang)

CodeReview時作為重點CR的地方

好的單測可作為指導文檔,方便使用者使用及閱讀

改動前:OSS文件夾概念是通過文件名創建的,下面改動前的方法入參是File,該方法可以正常使用,但是在寫單測的時候,我發現使用文件有兩個成本:坑:本地獲取的路徑與在容器獲取的路徑是不一致的,複雜度明顯增高。


 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();          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);}

改動後:將入參file修改為inputStream,這樣便可省去創建文件以及編寫獲取獲取文件路徑方法,同時還避免了獲取路徑的坑,一舉兩得,也通過單測找到了代碼設計不合理之處。


 private void uploadObject2Oss(OSS client, String bucketName, String dirName, InputStream is, String fileName,    long fileSize) throws Exception {        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);}

以下這個方法先不說可讀性問題,單從編寫單測來驗證邏輯是否正確,在寫單測時需要:

構造sourceInfos列表

構造String數組

構造map對象

構造List

構造User 對象

顯然這個方法是非常複雜的,但是邏輯就是得到一個指定長度列表。


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())));        if (resultList.size() < pageSize) {        compensate(resultList, pageSize, user.getAliuid());    }    return resultList;}

改動後:將入參改為List sourceInfos, int pageSize, String aliuid,將String[]改為SourceInfo,提升代碼可讀性,否則無從得知s[0]表示什麼,s[1]表示什麼,在寫單測時需要:經過改造,可測試性、可讀性均有提升,另外在這個例子中其實user對象只使用了aliuid,無需傳入整個對象,遵循KISS原則。


private List<String> getResultList(List<SourceInfo> sourceInfos, int pageSize, String aliuid) {        List<String> resultList = sourceInfos.stream()        .flatMap(sourceInfo -> {            int needNum = (int)(sourceInfo.getSourceRatio() * pageSize / 100);            return listSource(sourceInfo.getSourceChannel(), needNum, aliuid).stream();        }).collect(Collectors.toList());        compensate(resultList, pageSize, aliuid());    return resultList;}

工欲善其事必先利其器,抗拒寫單測的其中最主要的一個原因就是沒有神器在手!每次啟動應用動輒就是幾分鐘起,想要測試一個方法,上個廁所回來可能應用還沒啟動,如此低效,怎麼願意去寫,fast_tester只需要啟動應用一次(tip: 添加註解及測試方法需要重新啟動應用),支持測試代碼熱更新,後續可隨意編寫測試方法,一個字「秀」!
<dependency>    <groupId>com.alibaba</groupId>    <artifactId>fast-tester</artifactId>    <version>1.3</version>    <scope>test</scope></dependency>

(2)在test的package下創建TestApplication
@SpringBootApplicationpublic class TestApplication {    public static void main(String[] args){        PandoraBootstrap.run(args);        ConfigurableApplicationContext context = SpringApplication.run(TestApplication.class, args);                FastTester.run(context);    }}

@Slf4jpublic class BucketServiceTest {
@Autowired    BucketService bucketService;
@Test public void testSaveBucketInfo() { BucketRequest bucketRequest = new BucketRequest(); 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("缺少參數 result :{}", JSON.toJSONString(result)); bucketRequest.setBucketName("video2sky"); result = bucketService.saveBucketInfo(bucketRequest);        log.info("bucketName 重複 result :{}", JSON.toJSONString(result)); 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("缺少參數 result :{}", JSON.toJSONString(result)); bucketFolderRequest.setBucketId(1L); result = bucketService.createBucketFolder(bucketFolderRequest); log.info("錯誤的bucketId result :{}", JSON.toJSONString(result)); bucketFolderRequest.setWriteTokenExpireTime(7300L); result = bucketService.createBucketFolder(bucketFolderRequest); log.info("異常的讀時間 result :{}", JSON.toJSONString(result)); bucketFolderRequest.setBucketId(11L); bucketFolderRequest.setWriteTokenExpireTime(3500L); result = bucketService.createBucketFolder(bucketFolderRequest); log.info("重複的bucketFolder result :{}", JSON.toJSONString(result)); bucketFolderRequest.setFolderPath("/test2"); result = bucketService.createBucketFolder(bucketFolderRequest); log.info("正例 result :{}", JSON.toJSONString(result));    }}

(4)啟動TestApplication,輸入對應類名,選擇要執行的相應方法即可(切換測試類,直接重新輸入類路徑(包名+文件名)即可,原理還是反射)。

Tip:如果service註解失敗,檢查測試包的層級,例如:

JUnit是一個Java語言的單元測試框架, Junit測試是程式設計師測試,即所謂白盒測試,因為程式設計師知道被測試的軟體如何(How)完成功能和完成什麼樣(What)的功能。繼承TestCase類,就可以用Junit進行自動測試。來源:https://baike.baidu.com/item/白盒測試
@Slf4jpublic 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)相關測試註解如@Ignore使用,相關屬性如timeout測試接口性能、expected異常期望返回結果使用,測試全部測試方法等。
@Slf4jpublic class DateUtilTest {
@Ignore @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);    }
@Test(timeout = 110L, expected = ParseException.class) public void testString2Date() throws ParseException{ Date date = DateUtil.string2Date("20202-02 02:02"); log.info("date:{}" , date);     }
@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) { 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.htmlMockito是一個針對Java的mocking框架,主要作用mock請求及返回值。Mockito可以隔離類之間的相互依賴,做到真正的方法級別單測。
<dependency>   <groupId>org.mockito</groupId>   <artifactId>mockito-all</artifactId>   <version>1.9.5</version>   <scope>test</scope></dependency>

需要測試的方法中調用了二方/三方接口,而接口無測試環境,為了測試方法邏輯,可以模擬接口返回結果(對原先代碼無侵入),達到應用內測試閉環。tip:mock數據並非真正的返回值,需要注意返回的結果類型,字符串長度等,防止出現轉化,入庫欄位超長等問題。
@Overridepublic ConsumeCodeResult consumeCode(String code) {        if (code.startsWith(BENEFIT_CENTER_CODE_HEADER) && BENEFIT_CENTER_CODE_LENGTH == code.length()) {        return consumeCodeFromCodeBenefitCenter(code);    }        return consumeCodeFromCodeCenter(code);}
private ConsumeCodeResult consumeCodeFromCodeBenefitCenter(String code) { 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); }}

@Testpublic 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");     
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的功能非常多,可以驗證行為,做測試樁,匹配參數,驗證調用次數和執行順序等等,在這不一一枚舉了,更多詳細使用可見文檔:https://github.com/hehonghui/mockito-doc-zh覆蓋率是度量測試完整性的一個手段,是測試有效性的一個度量。是否覆蓋率越高越好?回歸根本,我們寫單測的意義最重要的一點是為了保證代碼的正確性,如果我們把複雜的、重要的、必要的測試覆蓋到,即可保證應用的正確性,例如set、get方法,完全沒有必要寫單測,不必為了追求覆蓋率而刻意寫單測,尺度這個東西,無論何時何事都是要有分寸的。躬身入局,寫起來,會慢慢找到節奏的。測試工具是神兵利器,設計原則是內功心法,設計原則作為編寫代碼的指導思想,單元測試作為驗證代碼好壞的有效途徑,共同推動代碼演進。

團隊無單測習慣,個人是否follow

業務壓力大,覺得寫單測耗時

覺得可有可無

單測是一個程式設計師的自我修養

5 大學習階段、20 門免費課程、175 課時教學視頻、12 套自測考試,帶大家掌握使用虛擬機安裝Linux,以及Linux常用命令、文件及用戶管理、文本處理、Vim工具使用等。

點擊「閱讀原文」,去學習吧~

相關焦點

  • 如何編寫屬於你的第一個 Android 單元測試?
    ,並以一個簡單例子演示了如何編寫屬於你的第一個 Android 單元測試(kotlin 代碼)。有了單元測試,我們就可以更加大膽的進行重構,重構完只要跑一下單測驗證是否通過就可以了(適合小範圍的重構,大的重構可能就需要重寫單元測試了)  在設計測試用例的過程中,需要考慮到業務上的各種場景,有助於我們跳出代碼加深對業務的理解  單元測試要求被測試的代碼高內聚,低耦合,所以你在寫業務代碼的時候就要考慮到如何寫測試,或者反過來,先寫測試用例的話會讓你能夠寫出來結構性更好的代碼
  • 聊聊單元測試
    大多數人的理由是沒時間寫,任務太多。但是說實話,是真的沒時間嗎?Z哥認為真是由於沒時間而不寫單元測試的人絕對是少數。況且,導致沒時間很大原因可能就是花了太多時間在處理bug上。所以,很多人沒有把單元測試當作一個「工具」,而把它看作是一種「負擔」。在這種心態下,就算要寫單元測試,也是為了寫而寫。更可怕的是,通過mock工具,還真能給任意代碼寫單元測試。
  • Android單元測試實踐
    為什麼要引入單元測試  一般來說我們都不會寫單元測試,為什麼呢?因為要寫多餘的代碼,而且還要進行一些學習,入門有些門檻,所以一般在工程中都不會寫單元測試。那麼為什麼我決定要寫單元測試。  這篇文章看完並不會讓你完全掌握單元測試,但是會給你在單元測試的開始有一個好的指引  大大提高工作效率  單元的概念比較模糊,可以是一個方法,可以是一個時機,但是不是一整套環節,一整套環節那就是集成測試了。為什麼說大大提高了工作效率。
  • Python單元測試——深入理解unittest
    單元測試的重要性就不多說了,可惡的是python中有太多的單元測試框架和工具,什麼unittest, testtools, subunit, coverage, testrepository, nose, mox, mock, fixtures, discover,再加上setuptools, distutils等等這些,先不說如何寫單元測試,光是怎麼運行單元測試就有
  • .NET 項目中的單元測試
    .NET 項目中的單元測試Intro「不會寫單元測試的程式設計師不是合格的程式設計師,不寫單元測試的程式設計師不是優秀的工程師。」—— 一隻想要成為一個優秀程式設計師的渣逼程序猿。那麼問題來了,什麼是單元測試,如何做單元測試。
  • 當Espresso遇見Android單元測試
    如果依賴Android環境,但是沒有UI相關或者UI比較簡單(如點擊按鈕)的單元測試可以使用開源庫Robolectric解決依賴問題,使測試運行在JVM上,而非模擬器上,大大提高測試運行效率。但是如果測試UI相關比較複雜的代碼,又可以如何進行測試呢?
  • 單元測試常用的方法
    概述   工廠在組裝一臺電視機之前,會對每個元件都進行測試,這,就是單元測試。其實我們每天都在做單元測試。你寫了一個函數,除了極簡單的外,總是要執行一下,看看功能是否正常,有時還要想辦法輸出些數據,如彈出信息窗口什麼的,這,也是單元測試,我們把這種單元測試稱為臨時單元測試。
  • 如何對機器學習做單元測試
    那一年,我犯了很多大錯誤,這些錯誤不僅幫助我了解了ML,還幫助我了解了如何正確而穩健地設計這些系統。我在谷歌Brain學到的一個主要原則是,單元測試可以決定算法的成敗,可以為你節省數周的調試和訓練時間。然而,在如何為神經網絡代碼編寫單元測試方面,似乎沒有一個可靠的在線教程。即使是像OpenAI這樣的地方,也只是通過盯著他們代碼的每一行,並試著思考為什麼它會導致bug來發現bug的。
  • 解讀Android官方MVP項目單元測試
    Google在3月份推出了一個項目,用來介紹Android MVP架構的各種組合,可以認為是官方在這方面的最佳實踐。令人稱道的是除了MVP本身之外,這些工程配備了極其完善的單元測試用例,學習價值極高。本文著重針對todo-mvp的單元測試進行解讀。寫在前面1. 關於MVP關於MVP的介紹很多,這不是本文的重點,這裡列舉近期一些比較好的文章。2.
  • JUnit及其相關的單元測試技術
    【IT168 技術文檔】在實際的工作中,很多項目都沒有寫單元測試用例。寫單元測試用例常常是程式設計師十分厭倦的一個項目活動,很多人覺得沒有必要、浪費時間。所有這些都是因為沒有認識到測試的重要性:測試能夠使我們儘量早的發現程序的bug,一個bug被隱藏的時間越長,修復這個bug的代價就越大。
  • Android單元測試——初探
    本文主要包含以下內容:什麼是單元測試為什麼需要進行單元測試如何進行單元測試什麼是單元測試首先總結一下什麼是單元測試,單元測試中的單元在Android或Java中可以理解為某個類中的某一個方法,因此單元測試就是針對Android或Java中某個類中的某一個方法中的邏輯代碼進行驗證即測試該方法是不是可以正常工作
  • Task12: 單元測試
    11.單元測試本節代碼樣例見code/utest文件夾在日常開發中,我們通常需要針對現有的功能進行單元測試,以驗證開發的正確性。在go標準庫中有一個叫做testing的測試框架,可以進行單元測試,命令是go test xxx。測試文件通常是以xx_test.go命名,放在同一包下面。
  • 合格的後端Coder都應該寫好UT和Mock測試
    廣義的測試包括 UT、IT、壓力測試、硬體測試等等,這裡重點討論 Unit Test 即單元測試。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。
  • 單元測試可測試程式設計師代碼編寫的正確性,如何使用VS2019測試項目
    單元測試是指編寫代碼來驗證開發者編寫代碼的正確性,一般單元測試也是由開發者完成的,自已開發單元測試代碼來檢查自己編寫代碼的通過性。定義:單元測試是開發人員編寫的、用於檢測在特定條件下目標代碼正確性的代碼,單元測試是代碼級別的測試。
  • 一年級語文下冊單元測試,家長自己閱卷,看圖寫話真能打滿分?
    一年級語文下冊單元測試,家長自己閱卷,看圖寫話真能打滿分?一年級小朋友壓力還真不小,每個單元都有單元測試,由於單元測試試卷不止一套,老師也看不過來,要求家長自己閱卷,結果家長給學生最後一題看圖寫話直接打滿分!
  • 單元測試的藝術
    最近讀了《單元測試的藝術》一書,對單元測試、單元測試的好處及自動化測試過程有了更深的了解。1. 工作單元(Unit of Work):從調用系統的一個公共方法到產生一個測試可見的最終結果期間這個系統發生的行為的總稱。2.
  • Spring Boot 單元測試
    一、 單元測試的概念概念:單元測試(unit testing),是指對軟體中的最小可測試單元進行檢查和驗證。在Java中單元測試的最小單元是類。單元測試是開發者編寫的一小段代碼,用於檢驗被測代碼的一個很小的、很明確的功能是否正確。執行單元測試,就是為了證明這 段代碼的行為和我們期望是否一致。
  • Web 前端單元測試到底要怎麼寫?看這一篇就夠了
    隨著 Web 應用的複雜程度越來越高,很多公司越來越重視前端單元測試。我們看到的大多數教程都會講單元測試的重要性、一些有代表性的測試框架 api 怎麼使用,但在實際項目中單元測試要怎麼下手?測試用例應該包含哪些具體內容呢?  本文從一個真實的應用場景出發,從設計模式、代碼結構來分析單元測試應該包含哪些內容,具體測試用例怎麼寫,希望看到的童鞋都能有所收穫。
  • 四年級語文單元測試複習備考
    四年級第一單元要考好,抓住重點複習,很重要。作文。作文三十分。學什麼考什麼。學習好的孩子可以看看自己寫的第一單元習作。學習差的孩子,不會寫,要儘量寫一篇,再背下來。怎麼也寫不出來,家長就在網上搜一下《推薦一個好地方》,找一篇好習作,讓孩子記個大概。習作不能空白。孩子不會背,就讓試著回答問題,寫出來。我喜歡什麼地方?
  • 高手如何給 Spring MVC 做單元測試?
    (1)結果處理器,表示要對結果做點什麼事情(2)比如此處使用 MockMvcResultHandlers.print() 輸出整個響應結果信息8)MvcResult(1)單元測試執行結果-- spring 單元測試組件包 --><dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.7.RELEASE</version>