Spock自带的Mock用法

之前我们已经介绍了一段业务场景代码,即接口调用,用户服务需要调用用户中心接口获取用户信息,代码如下:

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
package com.spock.example.service;

import com.spock.example.dao.MoneyDAO;
import com.spock.example.dao.UserDAO;
import com.spock.example.dto.UserDTO;
import com.spock.example.vo.OrderVO;
import com.spock.example.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;

import java.math.BigDecimal;
import java.util.List;

/**
* 用户服务
*
* @author jinglv
* @date 2021/7/20 5:46 下午
*/
public class UserService {
@Autowired
private UserDAO userDAO;

@Autowired
private MoneyDAO moneyDAO;

/**
* 通过userId获取用户信息
*
* @param uid 用户id
* @return 返回用户信息
*/
public UserVO getUserById(int uid) {
List<UserDTO> users = userDAO.getUserInfo();
UserDTO userDTO = users.stream().filter(u -> u.getId() == uid).findFirst().orElse(null);
UserVO userVO = new UserVO();
if (null == userDTO) {
return userVO;
}
userVO.setId(userDTO.getId());
userVO.setName(userDTO.getName());
userVO.setSex(userDTO.getSex());
userVO.setAge(userDTO.getAge());
// 显示邮编
if ("上海".equals(userDTO.getProvince())) {
userVO.setAbbreviation("沪");
userVO.setPostCode(200000);
}
if ("北京".equals(userDTO.getProvince())) {
userVO.setAbbreviation("京");
userVO.setPostCode(100000);
}
// 手机号处理
if (null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())) {
userVO.setTelephone(userDTO.getTelephone().substring(0, 3) + "****" + userDTO.getTelephone().substring(7));
}
return userVO;
}
}

以上代码中userDAO是使用Spring注入的用户中心服务的实例对象,只有拿到了用户中心的返回的users,才能继续下面的逻辑(根据uid筛选用户,DTO和VO转换,邮编、手机号码处理等)

所以正常的做法是把userDao的getUserInfo()方法mock掉,模拟一个我们指定的值,因为我们真正关心的是拿到users后自己代码的逻辑,这是我们需要重点验证的地方

Spock编写的测试代码如下:

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
package com.spock.example.groovy

import com.spock.example.dao.MoneyDAO
import com.spock.example.dao.UserDAO
import com.spock.example.dto.UserDTO
import com.spock.example.service.UserService
import com.spock.example.vo.OrderVO
import com.spock.example.vo.UserVO
import spock.lang.Specification

/**
* 用户服务测试类
* @author jinglv* @date 2021/7/20 6:02 下午
*/
class UserServiceTest extends Specification {
def userService = new UserService()
def userDAO = Mock(UserDAO)

void setup() {
userService.userDAO = userDAO
}

def "GetUserById"() {
given: "设置请求参数"
def user1 = new UserDTO(id: 1, name: "张三", province: "上海")
def user2 = new UserDTO(id: 2, name: "李四", province: "江苏")

and: "mock掉接口返回的用户信息"
userDAO.getUserInfo() >> [user1, user2]

when: "调用获取用户信息方法"
def response = userService.getUserById(1)

then: "验证返回结果是否符合预期值"
with(response) {
name == "张三"
abbreviation == "沪"
postCode == 200000
}
}
}

代码讲解

  • def userDAO = Mock(UserDAO)这一行代码使用spock自带的Mock方法构造一个userDAO的mock对象,如果要模拟userDAO方法的返回,只需userDAO.方法名() >> “模拟值”的方式,两个右箭头即可

  • setup方法是每个测试用例运行前的初始方法,类似于JUnit的@Before

  • “GetUserById”方法是单测的主要方法,可以看到分为4个模块:given、and、when、then,用来区分不同单测代码的作用:

    • given: 输入条件(前置参数)
    • when: 执行行为(mock接口、真实调用)
    • then: 输出条件(验证结果)
    • and: 衔接上个标签,补充的作用

    每个标签后面的双引号里可以添加描述,说明这块代码的作用(非强制),如”when: “调用获取用户信息方法””

因为spock使用Groovy作为单测开发语言,所以代码量上比使用java写的会少很多,比如given模块里通过构造函数的方式创建请求对象

1
2
3
given: "设置请求参数"
def user1 = new UserDTO(id: 1, name: "张三", province: "上海")
def user2 = new UserDTO(id: 2, name: "李四", province: "江苏")

实际上UserDTO.java 这个类并没有3个参数的构造函数,是Groovy帮我们实现的,Groovy默认会提供一个包含所有对象属性的构造函数

而且调用方式上可以指定属性名,类似于key:value的语法,非常人性化,方便我们在属性多的情况下构造对象,如果使用java写,可能就要调用很多setXXX()方法才能完成对象初始化的工作

1
2
and: "mock掉接口返回的用户信息"
userDAO.getUserInfo() >> [user1, user2]

这个就是spock的mock用法,即当调用userDao.getUserInfo()方法时返回一个List,list的创建也很简单,中括号”[]”即表示list,Groovy会根据方法的返回类型自动匹配是数组还是list,而list里的对象就是之前given块里构造的user对象

>>是指定返回结果,类似Mockito的when().thenReturn()语法,但更简洁一些,如果要指定返回多个值的话可以使用3个右箭头>>>,比如:

1
userDao.getUserInfo() >>> [[user1,user2],[user3,user4],[user5,user6]]

也可以写成这样:

1
userDao.getUserInfo() >> [user1,user2] >> [user3,user4] >> [user5,user6]

即每次调用userDao.getUserInfo()方法返回不同的值

如果mock的方法带有入参的话,比如下面的业务代码:

1
2
3
4
5
public List<UserDTO> getUserInfo(String uid){
// 模拟用户中心服务接口调用
List<UserDTO> users = new ArrayList<>();
return users;
}

这个getUserInfo(String uid)方法,有个参数uid,这种情况下如果使用spock的mock模拟调用的话,可以使用下划线_匹配参数,表示任何类型的参数,多个逗号隔开,类似与Mockito的any()方法

如果类中存在多个同名函数,可以通过 “_ as 参数类型” 的方式区别调用,类似下面的语法:

1
2
3
4
5
// _ 表示匹配任意类型参数
List<UserDTO> users = userDao.getUserInfo(_);

// 如果有同名的方法,使用as指定参数类型区分
List<UserDTO> users = userDao.getUserInfo(_ as String);

when模块里是真正调用要测试方法的入口:userService.getUserById()

then模块作用是验证被测方法的结果是否正确,符合预期值,所以这个模块里的语句必须是boolean表达式,类似于junit的assert断言机制,但你不必显示的写assert,这也是一种约定优于配置的思想

then块中使用了spock的with功能,可以验证返回结果response对象内部的多个属性是否符合预期值,这个相对于junit的assertNotNull或assertEquals的方式更简单一些

where用法讲解

业务代码中有三个if判断,分别是对邮编和手机号的处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 显示邮编
if ("上海".equals(userDTO.getProvince())) {
userVO.setAbbreviation("沪");
userVO.setPostCode(200000);
}
if ("北京".equals(userDTO.getProvince())) {
userVO.setAbbreviation("京");
userVO.setPostCode(100000);
}
// 手机号处理
if (null != userDTO.getTelephone() && !"".equals(userDTO.getTelephone())) {
userVO.setTelephone(userDTO.getTelephone().substring(0, 3) + "****" + userDTO.getTelephone().substring(7));
}

如果以Junit的单元测试,要覆盖着3个分支,就需要构造不同的请求参数多次调用被测试方法才能走到不同的分支,之前已经介绍了Spock的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
@Unroll
def "当输入的用户id为:#uid 时返回的邮编是:#postCodeResult,处理后的电话号码是:#telephoneResult"() {
given: "mock掉接口返回的用户信息"
userDAO.getUserInfo() >> users

when: "调用获取用户信息方法"
def response = userService.getUserById(uid)

then: "验证返回结果是否符合预期值"
with(response) {
postCode == postCodeResult
telephone == telephoneResult
}

where: "表格方式验证用户信息的分支场景"
uid | users || postCodeResult | telephoneResult
1 | getUser("上海", "13866667777") || 200000 | "138****7777"
1 | getUser("北京", "13811112222") || 100000 | "138****2222"
2 | getUser("南京", "13833334444") || 0 | null
}

def getUser(String province, String telephone) {
return [new UserDTO(id: 1, name: "张三", province: province, telephone: telephone)]
}

where模块第一行代码是表格的列名,多个列使用”|”单竖线隔开,”||”双竖线区分输入和输出变量,即左边是输入值,右边是输出值

格式如下:

输入参数1 | 输入参数2 || 输出结果1 | 输出结果2

表格的每一行代表一个测试用例,即被测方法被测试了3次,每次的输入和输出都不一样,刚好可以覆盖全部分支情况

比如uid、users都是输入条件,其中users对象的构造调用了getUser方法,每次测试业务代码传入不同的user值,postCodeResult、telephoneResult表示对返回的response对象的属性判断是否正确

第一行数据的作用是验证返回的邮编是否是”200000”,第二行是验证邮编是否是”100000”,第三行的邮编是否是”0”(因为代码里没有对南京的邮编进行处理,所以默认值是0)

这个就是where+with的用法,更符合我们实际测试的场景,既能覆盖多种分支,又可以对复杂对象的属性进行验证

其中在第2行定义的测试方法名是使用了groovy的字面值特性:

1
2
@Unroll
def "当输入的用户id为:#uid 时返回的邮编是:#postCodeResult,处理后的电话号码是:#telephoneResult"() {

即把请求参数值和返回结果值的字符串里动态替换掉,”#uid、#postCodeResult、#telephoneResult” 井号后面的变量是在方法内部定义的,前面加上#号,实现占位符的功能

@Unroll注解,可以把每一次调用作为一个单独的测试用例运行,这样运行后的单测结果更直观:

image-20210721162029985

而且其中一行测试结果不对,spock的错误提示信息也很详细,方便排查(比如我们把第2条测试用例返回的邮编改成”100001”):

image-20210721162213109

可以看出第2条测试用例失败,错误信息是postCodeResult的预期结果和实际结果不符,业务代码逻辑返回的邮编是”100000”,而我们预期的邮编是”100001”,这样你就可以排查是业务代码逻辑有问题还是我们的断言不对

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