背景

有些方法需要抛出异常来中断或控制流程,比如参数校验的逻辑: 不能为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
/**
* 校验请求参数user是否合法
*
* @param userVO 用户对象
* @throws ApiException 自定义业务对象
*/
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;

/**
* 自定义业务异常
*
* @author jinglv
* @date 2021/7/22 11:44 上午
*/
public class ApiException extends RuntimeException {
/**
* 业务错误Code码
*/
private String errorCode;
/**
* 业务错误信息
*/
private String errorMessage;

/**
* 构造函数
*
* @param errorCode 业务错误Code码
* @param 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;

/**
* @author jinglv
* @date 2021/7/22 3:43 下午
*/
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;

/**
* @author jinglv
* @date 2021/7/22 5:53 下午
*/
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) 注解。

但这两种方式都有缺陷:

  • @Test方式不能指定断言的异常属性,比如errorCode,errorMessage

  • ExpectedException的方式也只提供了expectMessage的api,对自定义的errorCode不支持,尤其像上面的有很多分支抛出多种不同异常码的情况

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

/**
* 校验用户请求参数的测试类
* @author jinglv* @date 2021/7/22 6:07 下午
*/
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是否符合预期值

image-20210722181550978

另外在where标签里构造请求参数时调用的getUser()方法使用了groovy的闭包功能,即case里面的condition1,condition2的写法

groovy的闭包(closure) 类似Java的lambda表达式,这样写主要是为了复用之前的请求参数,所以使用了闭包,当然也可以使用传统的new对象之后,setXXX的方式构造请求对象

学习大佬文章:https://javakk.com/292.html