Spock單元測試框架實戰指南四 - 異常測試

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一篇主要講使用Spock如何測試代碼中拋異常的場景","attrs":{}}]},{"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":"有些方法需要拋出異常來中斷或控制流程,比如參數校驗的邏輯: 不能爲null,不符合指定的類型,list不能爲空等驗證,如果校驗不通過則拋出checked異常,這個異常一般都是我們封裝的業務異常信息,比如下面的業務代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * 校驗請求參數user是否合法\n * @param user\n * @throws APIException\n */\npublic void validateUser(UserVO user) throws APIException {\n if(user == null){\n throw new APIException(\"10001\", \"user is null\");\n }\n if(null == user.getName() || \"\".equals(user.getName())){\n throw new APIException(\"10002\", \"user name is null\");\n }\n if(user.getAge() == 0){\n throw new APIException(\"10003\", \"user age is null\");\n }\n if(null == user.getTelephone() || \"\".equals(user.getTelephone())){\n throw new APIException(\"10004\", \"user telephone is null\");\n }\n if(null == user.getSex() || \"\".equals(user.getSex())){\n throw new APIException(\"10005\", \"user sex is null\");\n }\n if(null == user.getUserOrders() || user.getUserOrders().size() <= 0){\n throw new APIException(\"10006\", \"user order is null\");\n }\n for(OrderVO order : user.getUserOrders()) {\n if (null == order.getOrderNum() || \"\".equals(order.getOrderNum())) {\n throw new APIException(\"10007\", \"order number is null\");\n }\n if (null == order.getAmount()) {\n throw new APIException(\"10008\", \"order amount is null\");\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":"codeinline","content":[{"type":"text","text":"APIException","attrs":{}}],"attrs":{}},{"type":"text","text":"是我們封裝的業務異常,主要包含","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"errorCode","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"errorMessage","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 */\npublic class APIException extends RuntimeException {\n private String errorCode;\n private String errorMessage;\n\n setXXX...\n getXXX...\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的方式測試,會比較麻煩,就目前我使用過的方法,如果是單個異常還好,多個的就不太好寫測試代碼","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":"@Test \npublic void testException() {\n UserVO user = null;\n try {\n validateUser(user);\n } catch (APIException e) {\n assertThat(e.getErrorCode(), \"10001\");\n assertThat(e.getErrorMessage(), \"user is null\");\n }\n\n UserVO user = new UserVO();\n try {\n validateUser(user);\n } catch (APIException e) {\n assertThat(e.getErrorCode(), \"10002\");\n assertThat(e.getErrorMessage(), \"user name is null\");\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":"當然可以使用junit的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"ExpectedException","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":"@Rule\npublic [ExpectedException](http://javakk.com/tag/expectedexception \"查看更多關於 ExpectedException 的文章\") exception = ExpectedException.none();\nexception.expect(APIException.class); // 驗證拋出異常的類型是否符合預期\nexception.expectMessage(\"Order Flight return null exception\"); //驗證拋出異常的錯誤信息","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":"@Test(expected = APIException.class) ","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":"codeinline","content":[{"type":"text","text":"@Test","attrs":{}}],"attrs":{}},{"type":"text","text":"方式不能指定斷言的異常屬性,比如errorCode,errorMessage","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":"ExpectedException","attrs":{}}],"attrs":{}},{"type":"text","text":"的方式也只提供了expectMessage的api,對自定義的errorCode不支持,尤其像上面的有很多分支拋出多種不同異常碼的情況","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"thrown","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是如何解決的,Spock內置thrown()方法,可以捕獲調用業務代碼拋出的預期異常並驗證,再結合where表格的功能,可以很方便的覆蓋多種自定義業務異常,代碼如下:","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 */\nclass UserControllerTest extends Specification {\n\n def userController = new UserController()\n\n @Unroll\n def \"驗證用戶信息的合法性: #expectedMessage\"() {\n when: \"調用校驗用戶方法\"\n userController.validateUser(user)\n\n then: \"捕獲異常並設置需要驗證的異常值\"\n def exception = thrown(expectedException)\n exception.errorCode == expectedErrCode\n exception.errorMessage == expectedMessage\n\n where: \"表格方式驗證用戶信息的合法性\"\n user || expectedException | expectedErrCode | expectedMessage\n getUser(10001) || APIException | \"10001\" | \"user is null\"\n getUser(10002) || APIException | \"10002\" | \"user name is null\"\n getUser(10003) || APIException | \"10003\" | \"user age is null\"\n getUser(10004) || APIException | \"10004\" | \"user telephone is null\"\n getUser(10005) || APIException | \"10005\" | \"user sex is null\"\n getUser(10006) || APIException | \"10006\" | \"user order is null\"\n getUser(10007) || APIException | \"10007\" | \"order number is null\"\n getUser(10008) || APIException | \"10008\" | \"order amount is null\"\n }\n\n def getUser(errCode) {\n def user = new UserVO()\n def condition1 = {\n user.name = \"杜蘭特\"\n }\n def condition2 = {\n user.age = 20\n }\n def condition3 = {\n user.telephone = \"15801833812\"\n }\n def condition4 = {\n user.sex = \"男\"\n }\n def condition5 = {\n user.userOrders = [new OrderVO()]\n }\n def condition6 = {\n user.userOrders = [new OrderVO(orderNum: \"123456\")]\n }\n\n switch (errCode) {\n case 10001:\n user = null\n break\n case 10002:\n user = new UserVO()\n break\n case 10003:\n condition1()\n break\n case 10004:\n condition1()\n condition2()\n break\n case 10005:\n condition1()\n condition2()\n condition3()\n break\n case 10006:\n condition1()\n condition2()\n condition3()\n condition4()\n break\n case 10007:\n condition1()\n condition2()\n condition3()\n condition4()\n condition5()\n break\n case 10008:\n condition1()\n condition2()\n condition3()\n condition4()\n condition5()\n condition6()\n break\n }\n return user\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":"主要代碼就是在\"驗證用戶信息的合法性\"的測試方法裏,其中在","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"then","attrs":{}}],"attrs":{}},{"type":"text","text":"標籤裏用到了Spock的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"thrown()","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":"thrown","attrs":{}}],"attrs":{}},{"type":"text","text":"方法的入參","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"expectedException","attrs":{}}],"attrs":{}},{"type":"text","text":",是我們自己定義的異常變量,這個變量放在where標籤裏就可以實現驗證多種異常情況的功能(intellij idea格式化快捷鍵可以自動對齊表格)","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":"expectedException","attrs":{}}],"attrs":{}},{"type":"text","text":"的類型是我們調用的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"validateUser","attrs":{}}],"attrs":{}},{"type":"text","text":"方法裏定義的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"APIException","attrs":{}}],"attrs":{}},{"type":"text","text":"異常,我們可以驗證它的所有屬性,errorCode、errorMessage是否符合預期值","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/b1/b1180e29df0d6e6438b7b747ff7ef465.png","alt":"image","title":"image","style":null,"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":"另外在where標籤裏構造請求參數時調用的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"getUser()","attrs":{}}],"attrs":{}},{"type":"text","text":"方法使用了groovy的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"閉包","attrs":{}},{"type":"text","text":"功能,即case裏面的","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"condition1","attrs":{}}],"attrs":{}},{"type":"text","text":",","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"condition2","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":"groovy的閉包(closure) 類似Java的lambda表達式,這樣寫主要是爲了複用之前的請求參數,所以使用了閉包,當然也可以使用傳統的new對象之後,setXXX的方式構造請求對象","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/292.html","title":null},"content":[{"type":"text","text":"http://javakk.com/292.html","attrs":{}}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章