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":{}}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章