背景
有些方法需要抛出异常来中断或控制流程,比如参数校验的逻辑: 不能为null,不符合指定的类型,list不能为空等验证,如果校验不通过则抛出checked异常,这个异常一般都是我们封装的业务异常信息,比如下面的业务代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public void validateUser(UserVO userVO) throws ApiException { if (userVO == null) { throw new ApiException("10001", "User is null"); } if (null == userVO.getName() || "".equals(userVO.getName())) { throw new ApiException("10002", "User name is null"); } if (userVO.getAge() == 0) { throw new ApiException("10003", "User age is null"); } if (null == userVO.getTelephone() || "".equals(userVO.getTelephone())) { throw new ApiException("10004", "user telephone is null"); } if (null == userVO.getSex() || "".equals(userVO.getSex())) { throw new ApiException("10005", "user sex is null"); } if (null == userVO.getUserOrders() || userVO.getUserOrders().size() <= 0) { throw new ApiException("10006", "user order is null"); } for (OrderVO order : userVO.getUserOrders()) { if (null == order.getOrderNum() || "".equals(order.getOrderNum())) { throw new ApiException("10007", "order number is null"); } if (null == order.getAmount()) { throw new ApiException("10008", "order amount is null"); } } }
|
ApiException是我们封装的业务异常,主要包含errorCode,errorMessage属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.spock.example.exception;
public class ApiException extends RuntimeException {
private String errorCode;
private String errorMessage;
public ApiException(String errorCode, String errorMessage) { this.errorCode = errorCode; this.errorMessage = errorMessage; }
public String getErrorCode() { return errorCode; }
public String getErrorMessage() { return errorMessage; } }
|
针对抛出多个不同错误代码和错误信息的异常,如果我们使用Junit的方式测试,就会比较麻烦,最常见的单元测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package com.spock.example.controller;
import com.spock.example.exception.ApiException; import com.spock.example.vo.UserVO; import org.junit.Assert; import org.junit.Test;
public class UserControllerTest {
UserController userController = new UserController();
@Test public void testValidateUserIsNull() { UserVO userVO = null; try { userController.validateUser(userVO); } catch (ApiException e) { Assert.assertEquals(e.getErrorCode(), "10001"); Assert.assertEquals(e.getErrorMessage(), "User is null"); } }
@Test public void testValidateUserNameIsNull() { UserVO userVO = new UserVO(); try { userController.validateUser(userVO); } catch (ApiException e) { Assert.assertEquals(e.getErrorCode(), "10002"); Assert.assertEquals(e.getErrorMessage(), "User name is null"); } } …… }
|
也可以使用Junit的ExpectedException方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.spock.example.controller;
import com.spock.example.exception.ApiException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException;
public class UserControllerExceptionTest {
@Rule public ExpectedException exception = ExpectedException.none();
@Test public void testValidateUserIsNull() { exception.expect(ApiException.class); exception.expectMessage("Order Flight return null exception"); } }
|
或者使用@Test(expected = APIException.class) 注解。
但这两种方式都有缺陷:
Thrown
看下Spock是如何解决的,Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证,再结合where表格的功能,可以很方便的覆盖多种自定义业务异常,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| package com.spock.example
import com.spock.example.controller.UserController import com.spock.example.exception.ApiException import com.spock.example.vo.OrderVO import com.spock.example.vo.UserVO import spock.lang.Specification import spock.lang.Unroll
class UserControllerTest extends Specification {
def userController = new UserController()
@Unroll def "验证用户信息的合法性: #expectedMessage"() { when: "调用校验用户方法" userController.validateUser(user)
then: "捕获异常并设置需要验证的异常值" def exception = thrown(expectedException) exception.errorCode == expectedErrCode exception.errorMessage == expectedMessage
where: "表格方式验证用户信息的合法性" user || expectedException | expectedErrCode | expectedMessage getUser(10001) || ApiException | "10001" | "User is null" getUser(10002) || ApiException | "10002" | "User name is null" getUser(10003) || ApiException | "10003" | "User age is null" getUser(10004) || ApiException | "10004" | "User telephone is null" getUser(10005) || ApiException | "10005" | "User sex is null" getUser(10006) || ApiException | "10006" | "User order is null" getUser(10007) || ApiException | "10007" | "Order number is null" getUser(10008) || ApiException | "10008" | "Order amount is null" }
def getUser(errCode) { def user = new UserVO() def condition1 = { user.name = "杜兰特" } def condition2 = { user.age = 20 } def condition3 = { user.telephone = "15801833812" } def condition4 = { user.sex = "男" } def condition5 = { user.userOrders = [new OrderVO()] } def condition6 = { user.userOrders = [new OrderVO(orderNum: "123456")] } switch (errCode) { case 10001: user = null break case 10002: user = new UserVO() break case 10003: condition1() break case 10004: condition1() condition2() break case 10005: condition1() condition2() condition3() break case 10006: condition1() condition2() condition3() condition4() break case 10007: condition1() condition2() condition3() condition4() condition5() break case 10008: condition1() condition2() condition3() condition4() condition5() condition6() break } return user } }
|
主要代码就是在”验证用户信息的合法性”的测试方法里,其中在then标签里用到了Spock的thrown()方法,这个方法可以捕获我们要测试的业务代码里抛出的异常
thrown方法的入参expectedException,是我们自己定义的异常变量,这个变量放在where标签里就可以实现验证多种异常情况的功能
expectedException的类型是我们调用的validateUser方法里定义的APIException异常,我们可以验证它的所有属性,errorCode、errorMessage是否符合预期值

另外在where标签里构造请求参数时调用的getUser()方法使用了groovy的闭包功能,即case里面的condition1,condition2的写法
groovy的闭包(closure) 类似Java的lambda表达式,这样写主要是为了复用之前的请求参数,所以使用了闭包,当然也可以使用传统的new对象之后,setXXX的方式构造请求对象
学习大佬文章:https://javakk.com/292.html