Spock代码讲解 - 异常测试、void方法测试

Spock代码讲解 - 异常测试

异常方法测试

有些方法需要抛出异常来中断或控制流程,比如参数校验的逻辑: 不能为null,不符合指定的类型,list不能为空等验证,如果校验不通过则抛出checked异常,这个异常一般都是我们封装的业务异常信息,比如下面的业务代码:

/**
 * 校验请求参数user是否合法
 * @param user
 * @throws APIException
 */
public void validateUser(UserVO user) throws APIException {
    if(user == null){
        throw new APIException("10001", "user is null");
    }
    if(null == user.getName() || "".equals(user.getName())){
        throw new APIException("10002", "user name is null");
    }
    if(user.getAge() == 0){
        throw new APIException("10003", "user age is null");
    }
    if(null == user.getTelephone() || "".equals(user.getTelephone())){
        throw new APIException("10004", "user telephone is null");
    }
    if(null == user.getSex() || "".equals(user.getSex())){
        throw new APIException("10005", "user sex is null");
    }
    if(null == user.getUserOrders() || user.getUserOrders().size() <= 0){
        throw new APIException("10006", "user order is null");
    }
    for(OrderVO order : user.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属性:

/**
 * 自定义业务异常
 */
public class APIException extends RuntimeException {
    private String errorCode;
    private String errorMessage;

    setXXX...
    getXXX...
}

这个大家应该都很熟悉,针对这种抛出多个不同错误码和错误信息的异常,如果我们使用Junit的方式测试,会比较麻烦,就目前我使用过的方法,如果是单个异常还好,多个的就不太好写测试代码。

最常见的写法可能是下面这样:

@Test 
public void testException() {
  UserVO user = null;
  try {
    validateUser(user);
  } catch (APIException e) {
    assertThat(e.getErrorCode(), "10001");
    assertThat(e.getErrorMessage(), "user is null");
  }

  UserVO user = new UserVO();
  try {
    validateUser(user);
  } catch (APIException e) {
    assertThat(e.getErrorCode(), "10002");
    assertThat(e.getErrorMessage(), "user name is null");
  }
  ...
}

当然可以使用junit的ExpectedException方式:

@Rule
public ExpectedException exception = ExpectedException.none();
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表格的功能,可以很方便的覆盖多种自定义业务异常,代码如下:

/**
 * 校验用户请求参数的测试类
 * @author 公众号:Java老K
 * 个人博客:www.javakk.com
 */
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标签里就可以实现验证多种异常情况的功能(intellij idea格式化快捷键可以自动对齐表格)

expectedException的类型是我们调用的validateUser方法里定义的APIException异常,我们可以验证它的所有属性,errorCode、errorMessage是否符合预期值
Spock代码讲解 - 异常测试、void方法测试_第1张图片
另外在where标签里构造请求参数时调用的getUser()方法使用了groovy的闭包功能,即case里面的condition1,condition2的写法。

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

void方法测试

void方法的测试不能像前面几篇介绍的那样在then标签里验证返回结果,因为void方法没有返回值。

一般来说无返回值的方法,内部逻辑会修改入参的属性值,比如参数是个对象,那代码里可能会修改它的属性值,虽然没有返回,但还是可以通过校验入参的属性来测试void方法。

还有一种更有效的测试方式,就是验证方法内部逻辑和流程是否符合预期,比如:

  • 应该走到哪个分支逻辑?
  • 是否执行了这一行代码?
  • for循环中的代码执行了几次?
  • 变量在方法内部的变化情况?

先看一个void方法的业务代码示例:

/**
 * 根据汇率计算金额
 * @param userVO
 */
public void setOrderAmountByExchange(UserVO userVO){
    if(null == userVO.getUserOrders() || userVO.getUserOrders().size() <= 0){
        return ;
    }
    for(OrderVO orderVO : userVO.getUserOrders()){
        BigDecimal amount = orderVO.getAmount();
        // 获取汇率(调用汇率接口)
        BigDecimal exchange = moneyDAO.getExchangeByCountry(userVO.getCountry());
        amount = amount.multiply(exchange); // 根据汇率计算金额
        orderVO.setAmount(amount);
    }
}

这个void方法主要是遍历userVO下面的订单,通过调用汇率接口计算订单的外币金额,然后再赋值给userVO.orderVO.amount,所以他的核心逻辑在for循环里,那么我们的测试重点就是验证for循环里面的逻辑是否符合预期,金额计算是否正确。

代码实现

直接看Spock的测试代码如何写:

/**
 * 用户服务测试类
 * @author 公众号:Java老K
 * 个人博客:www.javakk.com
 */
class UserServiceTest extends Specification {
    def userService = new UserService()
    def moneyDAO = Mock(MoneyDAO)

    void setup() {
        userService.userDao = userDao
        userService.moneyDAO = moneyDAO
    }

    def "测试void方法"() {
        given: "设置请求参数"
        def userVO = new UserVO(name:"James", country: "美国")
        userVO.userOrders = [new OrderVO(orderNum: "1", amount: 10000), new OrderVO(orderNum: "2", amount: 1000)]

        when: "调用设置订单金额的方法"
        userService.setOrderAmountByExchange(userVO)

        then: "验证调用获取最新汇率接口的行为是否符合预期: 一共调用2次, 第一次输出的汇率是0.1413, 第二次是0.1421"
        2 * moneyDAO.getExchangeByCountry(_) >> 0.1413 >> 0.1421

        and: "验证根据汇率计算后的金额结果是否正确"
        with(userVO){
            userOrders[0].amount == 1413
            userOrders[1].amount == 142.1
        }
    }
}

主要是then标签里的语法: “2 * moneyDAO.getExchangeByCountry(_) >> 0.1413 >> 0.1421”,这行代码表示moneyDAO的getExchangeByCountry()方法会被执行2次,第一次输出的结果是0.1413,第二次输出的接口是0.1421

“2 * moneyDAO.getExchangeByCountry() >> 0.1413 >> 0.1421" 这行代码也可以分开写,比如只写前面的""2 * moneyDAO.getExchangeByCountry()”

"2 * " 表示方法实际执行的次数, 如果不是2次则不符合预期,单元测试会失败,看你具体的传参,比如在given标签里我们构造的user下面有2个order,订单号分别是1,2,金额分别是1w,1k

那么在调用void方法时,for循环就会循环2次,所以可以通过这样的写法验证我们调用汇率接口的方法是否执行了,以及执行次数

最后在with()方法里会对入参userVO里的订单金额amount进行校验,因为我们设置的两单订单金额分别是1w和1k,then标签已经对汇率接口的返回结果mock了2个不同的汇率值:0.1413、0.1421,那么转换后的外币金额就是1413和142.1元

(关注公众号: java老k 回复spock获取全部源码)

你也可以将代码改成"1 * moneyDAO.getExchangeByCountry(_)",然后运行单测,会提示相应的错误信息:
Spock代码讲解 - 异常测试、void方法测试_第2张图片
报错信息说明实际执行(invoke)了2次

执行几次就写几次,没有执行过就是"0 * ",这正是BDD行为驱动开发思想的体现

(power mock的thenVerify()也可以实现这样的功能,只不过Spock语法更简洁一些)

void + where

如果要结合where测试多分支的void方法时,需要注意一点,因为Spock要求where标签里的表格至少含有两列,如果你的where只是验证入参,也就是只有一列需要验证,那么可以用"_" 表示另外一列值,代码类似下面这样的写法:

where:
requsetParam | _
userVO1      | _
userVO2      | _
userVO3      | _

使用“_”表示任意输入或输出

文章来源:http://javakk.com/category/spock

互联网一线java开发老兵,工作10年有余,梦想敲一辈子代码,以梦为码,不负韶华!
Spock代码讲解 - 异常测试、void方法测试_第3张图片
扫码关注Java老K,获取更多Java技术干货。

你可能感兴趣的:(java,单元测试,后端编程,java,junit,groovy,单元测试,编程语言)