Spock單元測試框架實戰指南一Spock是什麼?它和JUnit有什麼區別?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是Spock系列的第一篇文章,整個專輯會介紹Spock的用途,爲什麼使用Spock?它能給我們帶來什麼好處?它和JUnit、JMock、Mockito有什麼區別?我們平時寫單元測試代碼的常見問題和痛點,Spock又是如何解決的,Spock的代碼怎麼編寫以及Spock的優勢和缺點等內容,讓大家對Spock有個客觀的瞭解。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Spock是什麼?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e9/e974a4d7dab15d5a52a0aa3390e81241.jpeg","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"斯波克是國外一款優秀的測試框架,基於","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"BDD思想","attrs":{}},{"type":"text","text":",功能強大,能夠讓我們的測試","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"代碼規範","attrs":{}},{"type":"text","text":"化,結構","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"層次清晰","attrs":{}},{"type":"text","text":",結合","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"groovy動態語言","attrs":{}},{"type":"text","text":"的特點以及自身提供的各種標籤讓編寫測試代碼更加","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"高效","attrs":{}},{"type":"text","text":"和","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"簡潔","attrs":{}},{"type":"text","text":",提供一種通用、簡單、結構化的描述語言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引用官網的介紹如下(","attrs":{}},{"type":"link","attrs":{"href":"http://javakk.com/redirect/aHR0cDovL3Nwb2NrZnJhbWV3b3JrLm9yZw==","title":null},"content":[{"type":"text","text":"http://spockframework.org","attrs":{}}]},{"type":"text","text":")","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/22/22680a59dbbf6e85ae0a7eb3ebc66b17.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/21/212bc60397df79cd8c18c2c6273a2e91.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"“ Spock是一個Java和Groovy應用程序的測試和規範框架。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它之所以能在人羣中脫穎而出,是因爲它優美而富有表現力的規範語言。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"斯波克的靈感來自JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans ”","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單說Spock的特點如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓我們的測試代碼更規範,內置多種標籤來規範單測代碼的語義,從而讓我們的測試代碼結構清晰,更具可讀性,降低後期維護難度","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供多種標籤,比如: ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"where","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"with","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"thrown","attrs":{}}],"attrs":{}},{"type":"text","text":"... 幫助我們應對複雜的測試場景","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再加上使用groovy這種動態語言來編寫測試代碼,可以讓我們編寫的測試代碼更簡潔,適合敏捷開發,提高編寫單測代碼的效率","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遵從BDD行爲驅動開發模式,不單是爲了測試覆蓋率而測試,有助於提升代碼質量","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"IDE兼容性好,自帶mock功能","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"爲什麼使用Spock? Spock和JUnit、JMock、Mockito的區別在哪裏?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"收到現有的單測框架比如junit、jmock、mockito都是相對獨立的工具,只是針對不同的業務場景提供特定的解決方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Junit單純用於測試,不提供mock功能","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務已經是互聯網公司的主流技術架構,大部分的系統都是分佈式,服務與服務之間一般通過接口的方式交互,甚至服務內部也劃分成多個module,很多業務功能需要依賴底層接口返回的數據才能繼續剩下的流程,或者從數據庫/Redis等存儲設備上獲取,或是從配置中心的某個配置獲取。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣就導致如果我們想要測試代碼邏輯是否正確,就必須把這些依賴項(接口、Redis、DB、配置中心...)給mock掉。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果接口不穩定或有問題則會影響我們代碼的正常測試,所以我們要把調用接口的地方給","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"模擬","attrs":{}},{"type":"text","text":"掉,讓它返回指定的結果(提前準備好的數據),這樣才能往下驗證我們自己的代碼是否正確,符合預期邏輯和結果。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JMock或Mockito雖然提供了mock功能,可以把接口等依賴屏蔽掉,但不提供對靜態類靜態方法的mock,PowerMock或Jmockit雖然提供靜態類和方法的mock,但它們之間需要整合(junit+mockito+powermock),語法繁瑣,而且這些工具並沒有告訴你“","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"單元測試代碼到底應該怎麼寫?","attrs":{}},{"type":"text","text":"”","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"工具多了也會導致不同的人寫出的單元測試代碼五花八門,風格迥異。。。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spock通過提供規範描述,定義多種標籤(","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"given","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"when","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"then","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"where","attrs":{}}],"attrs":{}},{"type":"text","text":"等)去描述代碼“應該做什麼”,輸入條件是什麼,輸出是否符合預期,從語義層面規範代碼的編寫。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spock自帶Mock功能,使用簡單方便(也支持擴展其他mock框架,比如power mock),再加上groovy動態語言的強大語法,能寫出簡潔高效的測試代碼,同時更方便直觀的驗證業務代碼行爲流轉,增強我們對代碼執行邏輯的可控性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景和初衷","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網上關於Spock的資料比較簡單,包括官網的demo,無法解決我們項目中的複雜業務場景,需要找到一套適合自己項目的成熟解決方案,所以覺得有必要把我們項目中使用Spock的經驗分享出來, 幫助大家提升單測開發的效率和驗證代碼質量。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在熟練掌握Spock後我們項目組整體的單測開發效率提升了50%以上,代碼可讀性和維護性都得到了改善和提升。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"適合人羣","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"寫Java單元測試的開發小夥伴和測試同學,所有的演示代碼運行在IntelliJ IDEA中,spring-boot項目,基於Spock 1.3-groovy-2.5版本","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Spock如何解決傳統單元測試開發中的痛點","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這篇主要講下我們平時寫單元測試過程中遇到的幾種常見問題,分別使用JUnit和Spock如何解決,通過對比的方式給大家一個整體認識。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一. 單元測試代碼開發的成本和效率","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複雜場景的業務代碼,在分支(","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"if/else","attrs":{}}],"attrs":{}},{"type":"text","text":")很多的情況下,編寫單測代碼的成本會相應增加,正常的業務代碼或許只有幾十行,但爲了測試這個功能,要覆蓋大部分的分支場景,寫的測試代碼可能遠遠不止幾十行","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉個我們生產環境前不久發生的一起事故:有個功能上線1年多一直都正常,沒有出過問題,但最近有個新的調用方請求的數據不一樣,走到了代碼中一個不常用的分支邏輯,導致了bug,直接拋出異常阻斷了主流程,好在調用方請求量不大。。。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"估計當初寫這段代碼的同學也認爲很小几率會走到這個分支,雖然當時也寫了單元測試代碼,但分支較多,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"剛好漏掉了這個分支邏輯的測試,給日後上線留下了隱患","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這也是我們平時寫單元測試最常遇到的問題:要達到分支覆蓋率高要求的情況下,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"if/else","attrs":{}}],"attrs":{}},{"type":"text","text":"有不同的結果,傳統的單測寫法可能要多次調用,才能覆蓋全部的分支場景,一個是寫單測麻煩,同時也會增加單測代碼的冗餘度","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然可以使用junit的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"@parametered","attrs":{}}],"attrs":{}},{"type":"text","text":"參數化註解或者dataprovider的方式,但還是不夠方便直觀,而且如果其中一次分支測試case出錯的情況下,報錯信息也不夠詳盡。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如下面的示例演示代碼,根據輸入的身份證號碼識別出生日期、性別、年齡等信息,這個方法的特點就是有很多","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"if...else...","attrs":{}}],"attrs":{}},{"type":"text","text":"的分支嵌套邏輯","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * 身份證號碼工具類

\n * 15位:6位地址碼+6位出生年月日(900101代表1990年1月1日出生)+3位順序碼\n * 18位:6位地址碼+8位出生年月日(19900101代表1990年1月1日出生)+3位順序碼+1位校驗碼\n * 順序碼奇數分給男性,偶數分給女性。\n * @author 公衆號:Java老K\n * 個人博客:www.javakk.com\n */\npublic class IDNumberUtils {\n /**\n * 通過身份證號碼獲取出生日期、性別、年齡\n * @param certificateNo\n * @return 返回的出生日期格式:1990-01-01 性別格式:F-女,M-男\n */\n public static Map getBirAgeSex(String certificateNo) {\n String birthday = \"\";\n String age = \"\";\n String sex = \"\";\n\n int year = Calendar.getInstance().get(Calendar.YEAR);\n char[] number = certificateNo.toCharArray();\n boolean flag = true;\n if (number.length == 15) {\n for (int x = 0; x < number.length; x++) {\n if (!flag) return new HashMap<>();\n flag = Character.isDigit(number[x]);\n }\n } else if (number.length == 18) {\n for (int x = 0; x < number.length - 1; x++) {\n if (!flag) return new HashMap<>();\n flag = Character.isDigit(number[x]);\n }\n }\n if (flag && certificateNo.length() == 15) {\n birthday = \"19\" + certificateNo.substring(6, 8) + \"-\"\n + certificateNo.substring(8, 10) + \"-\"\n + certificateNo.substring(10, 12);\n sex = Integer.parseInt(certificateNo.substring(certificateNo.length() - 3,\n certificateNo.length())) % 2 == 0 ? \"女\" : \"男\";\n age = (year - Integer.parseInt(\"19\" + certificateNo.substring(6, 8))) + \"\";\n } else if (flag && certificateNo.length() == 18) {\n birthday = certificateNo.substring(6, 10) + \"-\"\n + certificateNo.substring(10, 12) + \"-\"\n + certificateNo.substring(12, 14);\n sex = Integer.parseInt(certificateNo.substring(certificateNo.length() - 4,\n certificateNo.length() - 1)) % 2 == 0 ? \"女\" : \"男\";\n age = (year - Integer.parseInt(certificateNo.substring(6, 10))) + \"\";\n }\n Map map = new HashMap<>();\n map.put(\"birthday\", birthday);\n map.put(\"age\", age);\n map.put(\"sex\", sex);\n return map;\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對上面這種場景,spock提供了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"where","attrs":{}},{"type":"text","text":"標籤,讓我們可以通過表格的方式方便測試多種分支","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的對比圖是針對\"根據身份證號碼獲取出生日期、性別、年齡\"方法實現的單元測試,左邊是我們常用的Junit的寫法,右邊是Spock的寫法,紅框圈出來的是一樣的功能在Junit和Spock上的代碼實現 (兩邊執行的單測結果一樣,點擊放大查看差異)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/88/88c9d874ebb7858a2f60173af0d89326.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對比結果:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"右邊一欄使用Spock寫的單測代碼上語法簡潔,表格方式測試覆蓋多分支場景也更直觀,提升開發效率,更適合敏捷開發","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(關於Spock代碼的具體語法會在後續文章講解)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二. 單元測試代碼的可讀性和後期維護","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微服務架構下,很多場景需要依賴其他接口返回的結果才能驗證自己代碼的邏輯,這樣就需要使用mock工具,但JMock或Mockito的語法比較繁瑣,再加上單測代碼不像業務代碼那麼直觀,不能完全按照業務流程的思路寫單測,以及開發同學對單測代碼可讀性的不重視,最終導致測試代碼難於閱讀,維護起來更是難上加難","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可能自己寫完的測試,過幾天再看就雲裏霧裏了(當然添加註釋會好很多),再比如改了原來的代碼邏輯導致單測執行失敗,或者新增了分支邏輯,單測沒有覆蓋到,隨着後續版本的迭代,會導致單測代碼越來越臃腫和難以維護","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spock提供多種語義標籤,如: given、when、then、expect、where、with、and 等,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"從行爲上規範單測代碼","attrs":{}},{"type":"text","text":",每一種標籤對應一種語義,讓我們的單測代碼結構具有層次感,功能模塊劃分清晰,便於後期維護","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spock自帶mock功能,使用上簡單方便(Spock也支持擴展第三方mock框架,比如power mock)保證代碼更加規範,結構模塊化,邊界範圍清晰,可讀性強,便於擴展和維護,用自然語言描述測試步驟,讓非技術人員也能看懂測試代碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如下面的業務代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用用戶接口或者從數據庫獲取用戶信息,然後做一些轉換和判斷邏輯(這裏的業務代碼只是列舉常見的業務場景,方便演示)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * 用戶服務\n * @author 公衆號:Java老K\n * 個人博客:www.javakk.com\n */\n@Service\npublic class UserService {\n\n @Autowired\n UserDao userDao;\n\n @Autowired\n MoneyDAO moneyDAO;\n\n public UserVO getUserById(int uid){\n List users = userDao.getUserInfo();\n UserDTO userDTO = users.stream().filter(u -> u.getId() == uid).findFirst().orElse(null);\n UserVO userVO = new UserVO();\n if(null == userDTO){\n return userVO;\n }\n userVO.setId(userDTO.getId());\n userVO.setName(userDTO.getName());\n userVO.setSex(userDTO.getSex());\n userVO.setAge(userDTO.getAge());\n // 顯示郵編\n if(\"上海\".equals(userDTO.getProvince())){\n userVO.setAbbreviation(\"滬\");\n userVO.setPostCode(200000);\n }\n if(\"北京\".equals(userDTO.getProvince())){\n userVO.setAbbreviation(\"京\");\n userVO.setPostCode(100000);\n }\n // 手機號處理\n if(null != userDTO.getTelephone() && !\"\".equals(userDTO.getTelephone())){\n userVO.setTelephone(userDTO.getTelephone().substring(0,3)+\"****\"+userDTO.getTelephone().substring(7));\n }\n\n return userVO;\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的對比圖是分別使用Junit和Spock實現的單元測試,左邊是Junit的寫法,右邊是Spock,紅框圈出來的是一樣的功能在Junit和Spock上的實現 (兩邊執行的單測結果一樣,點擊放大查看差異)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4e/4e0ea09823e3973f7dfe48355201693d.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對比結果:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左邊的junit單測代碼冗餘,缺少結構層次,可讀性差,隨着後續迭代勢必會導致代碼的堆積,後期維護成本會越來越高。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"右邊的單測代碼spock會強制要求使用","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"given","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"when","attrs":{}}],"attrs":{}},{"type":"text","text":"、","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"then","attrs":{}}],"attrs":{}},{"type":"text","text":"這樣的語義標籤(至少一個),否則編譯不通過,這樣保證代碼更加","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"規範","attrs":{}},{"type":"text","text":",結構","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"模塊化","attrs":{}},{"type":"text","text":",邊界範圍清晰,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"可讀性強","attrs":{}},{"type":"text","text":",便於擴展和維護,用自然語言描述測試步驟,讓非技術人員也能看懂測試代碼(","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"given","attrs":{}}],"attrs":{}},{"type":"text","text":"表示輸入條件,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"when","attrs":{}}],"attrs":{}},{"type":"text","text":"觸發動作,","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"then","attrs":{}}],"attrs":{}},{"type":"text","text":"驗證輸出結果)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spock自帶的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"mock","attrs":{}}],"attrs":{}},{"type":"text","text":"語法也非常簡單:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"userDao.getUserInfo() >> [user1, user2]","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兩個右箭頭\">>\"表示即模擬","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"getUserInfo","attrs":{}}],"attrs":{}},{"type":"text","text":"接口的返回結果,再加上使用的groovy語言,可以直接使用\"[]\"中括號表示返回的是List類型(具體語法會在下一篇講到)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三. 單元測試不僅僅是爲了達到覆蓋率統計,更重要的是驗證業務代碼的健壯性、邏輯的嚴謹性以及設計的合理性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在項目初期爲了趕進度,可能沒時間寫單測,或者這個時期寫的單測只是爲了達到覆蓋率要求(因爲有些公司在發佈前會使用jacoco等單測覆蓋率工具來設置一個標準,比如新增代碼必須達到80%的覆蓋率才能發佈)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再加上傳統的單測是使用java這種強類型語言寫的,以及各種底層接口的mock導致寫起單測來繁瑣費時","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時寫的單測代碼比較粗糙,顆粒度比較大,缺少對單測結果值的有效驗證,這樣的單元測試對代碼質量的驗證和提升無法完全發揮作用,更多的是爲了測試而測試","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後大家不得不接受“","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"雖然寫了單測,但卻沒什麼鳥用","attrs":{}},{"type":"text","text":"”的結果","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如下面這段業務代碼示例:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"void","attrs":{}}],"attrs":{}},{"type":"text","text":"方法,沒有返回結果,如何寫單測測試這段代碼的邏輯是否正確?即如何知道單測代碼是否執行到了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"for","attrs":{}}],"attrs":{}},{"type":"text","text":"循環裏面的語句(可以通過查看覆蓋率或打斷點的方式確認,但這樣太麻煩了),如何確保循環裏面的金額是否計算正確?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家可以想下使用junit的方式寫單元測試如何驗證這幾點?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * 用戶服務\n * @author 公衆號:Java老K\n * 個人博客:www.javakk.com\n */\n@Service\npublic class UserService {\n\n @Autowired\n MoneyDAO moneyDAO;\n\n /**\n * 根據匯率計算金額\n * @param userVO\n */\n public void setOrderAmountByExchange(UserVO userVO){\n if(null == userVO.getUserOrders() || userVO.getUserOrders().size() <= 0){\n return ;\n }\n for(OrderVO orderVO : userVO.getUserOrders()){\n BigDecimal amount = orderVO.getAmount();\n // 獲取匯率(調用匯率接口)\n BigDecimal exchange = moneyDAO.getExchangeByCountry(userVO.getCountry());\n amount = amount.multiply(exchange); // 根據匯率計算金額\n orderVO.setAmount(amount);\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用Spock寫的話就會方便很多,如下圖所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8d/8dad42a0239298be4c9b641022a5c74e.png","alt":"image","title":"image","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"2 * moneyDAO.getExchangeByCountry(_) >> 0.1413 >> 0.1421","attrs":{}}],"attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這行代碼表示在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"for","attrs":{}}],"attrs":{}},{"type":"text","text":"循環中一共調用了2次獲取匯率的接口,第一次匯率結果是0.1413,第二次是0.1421,(模擬匯率接口的實時變動),然後在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"with","attrs":{}}],"attrs":{}},{"type":"text","text":"裏驗證,類似於junit裏的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"assert","attrs":{}}],"attrs":{}},{"type":"text","text":"斷言,驗證匯率折算後的人民幣價格是否正確(完整代碼會在後續文章中列出)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的好處就是:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提升單測代碼的可控性,方便驗證業務代碼的邏輯正確和是否合理, 這正是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"BDD(行爲驅動開發)","attrs":{}},{"type":"text","text":"思想的一種體現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲代碼的可測試性是衡量代碼質量的重要標準, 如果代碼不容易測試, 那就要考慮重構了, 這也是單元測試的一種正向作用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一篇文章從3個方面對比展示了Spock的特點和優勢,後面會詳細講解Spock的各種用法(結合具體的業務場景),以及groovy的一些語法和注意事項","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章來源:","attrs":{}},{"type":"link","attrs":{"href":"http://javakk.com/264.html","title":null},"content":[{"type":"text","text":"http://javakk.com/264.html","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章