Mock模拟
考虑如下场景,代码如下:
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
public StudentVO getStudentById(int id) {
List<StudentDTO> students = studentDao.getStudentInfo();
StudentDTO studentDTO = students.stream().filter(u -> u.getId() == id).findFirst().orElse(null);
StudentVO studentVO = new StudentVO();
if (studentDTO == null) {
return studentVO;
}
studentVO.setId(studentDTO.getId());
studentVO.setName(studentDTO.getName());
studentVO.setSex(studentDTO.getSex());
studentVO.setAge(studentDTO.getAge());
// 邮编
if ("上海".equals(studentDTO.getProvince())) {
studentVO.setAbbreviation("沪");
studentVO.setPostCode("200000");
}
if ("北京".equals(studentDTO.getProvince())) {
studentVO.setAbbreviation("京");
studentVO.setPostCode("100000");
}
return studentVO;
}
}
其中studentDao是使用Spring注入的实例对象,我们只有拿到了返回的students,才能继续下面的逻辑(根据id筛选学生,DTO和VO转换,邮编等)。所以正常的做法是把studentDao的getStudentInfo()方法Mock掉,模拟一个指定的值,因为我们真正关心的是拿到students后自己代码的逻辑,这是需要重点验证的地方。按照上面的思路使用Spock编写的测试代码如下:
class StudentServiceSpec extends Specification {
def studentDao = Mock(StudentDao)
def tester = new StudentService(studentDao: studentDao)
def "test getStudentById"() {
given: "设置请求参数"
def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")
and: "mock studentDao返回值"
studentDao.getStudentInfo() >> [student1, student2]
when: "获取学生信息"
def response = tester.getStudentById(1)
then: "结果验证"
with(response) {
id == 1
abbreviation == "京"
postCode == "100000"
}
}
}
这里主要讲解Spock的代码(从上往下)。
def studentDao = Mock(StudentDao) 这一行代码使用Spock自带的Mock方法,构造一个studentDao的Mock对象,如果要模拟studentDao方法的返回,只需studentDao.方法名() >> "模拟值"的方式,两个右箭头的方式即可。test getStudentById方法是单元测试的主要方法,可以看到分为4个模块:given、and、when、then,用来区分不同单元测试代码的作用:
given:输入条件(前置参数)。
when:执行行为(Mock接口、真实调用)。
then:输出条件(验证结果)。
and:衔接上个标签,补充的作用。
每个标签后面的双引号里可以添加描述,说明这块代码的作用(非强制),如when:"获取信息"。因为Spock使用Groovy作为单元测试开发语言,所以代码量上比使用Java写的会少很多,比如given模块里通过构造函数的方式创建请求对象。
实际上StudentDTO.java 这个类并没有3个参数的构造方法,是Groovy帮我们实现的。Groovy默认会提供一个包含所有对象属性的构造方法。而且调用方式上可以指定属性名,类似于key:value的语法,非常人性化,方便在属性多的情况下构造对象,如果使用Java写,可能就要调用很多的setXxx()方法,才能完成对象初始化的工作。
这个就是Spock的Mock用法,当调用studentDao.getStudentInfo()方法时返回一个List。List的创建也很简单,中括号[]即表示List,Groovy会根据方法的返回类型,自动匹配是数组还是List,而List里的对象就是之前given块里构造的user对象,其中 >> 就是指定返回结果,类似Mockito的when().thenReturn()语法,但更简洁一些。
如果要指定返回多个值的话,可以使用3个右箭头>>>,比如:studentDao.getStudentInfo() >>> [[student1,student2],[student3,student4],[student5,student6]]。
也可以写成这样:studentDao.getStudentInfo() >> [student1,student2] >> [student3,student4] >> [student5,student6]。
每次调用studentDao.getStudentInfo()方法返回不同的值。
public List<StudentDTO> getStudentInfo(String id){
List<StudentDTO> students = new ArrayList<>();
return students;
}
这个getStudentInfo(String id)方法,有个参数id,这种情况下如果使用Spock的Mock模拟调用的话,可以使用下划线_匹配参数,表示任何类型的参数,多个逗号隔开,类似于Mockito的any()方法。如果类中存在多个同名方法,可以通过 _ as参数类型 的方式区别调用,如下面的语法:
// _ 表示匹配任意类型参数
List<StudentDTO> students = studentDao.getStudentInfo(_);
// 如果有同名的方法,使用as指定参数类型区分
List<StudentDTO> students = studentDao.getStudentInfo(_ as String);
when模块里是真正调用要测试方法的入口tester.getStudentById()。then模块作用是验证被测方法的结果是否正确,符合预期值,所以这个模块里的语句必须是boolean表达式,类似于JUnit的assert断言机制,但不必显示地写assert,这也是一种约定优于配置的思想。then块中使用了Spock的with功能,可以验证返回结果response对象内部的多个属性是否符合预期值,这个相对于JUnit的assertNotNull或assertEquals的方式更简单一些。
强大的Where
上面的业务代码有2个if判断,是对邮编处理逻辑:
// 邮编
if ("上海".equals(studentDTO.getProvince())) {
studentVO.setAbbreviation("沪");
studentVO.setPostCode("200000");
}
if ("北京".equals(studentDTO.getProvince())) {
studentVO.setAbbreviation("京");
studentVO.setPostCode("100000");
}
如果要完全覆盖这2个分支就需要构造不同的请求参数,多次调用被测试方法才能走到不同的分支。在前面,我们介绍了Spock的where标签可以很方便的实现这种功能,代码如下所示:
@Unroll
def "input 学生id:#id, 返回的邮编:#postCodeResult, 返回的省份简称:#abbreviationResult"() {
given: "Mock返回的学生信息"
studentDao.getStudentInfo() >> students
when: "获取学生信息"
def response = tester.getStudentById(id)
then: "验证返回结果"
with(response) {
postCode == postCodeResult
abbreviation == abbreviationResult
}
where: "经典之处:表格方式验证学生信息的分支场景"
id | students || postCodeResult | abbreviationResult
1 | getStudent(1, "张三", "北京") || "100000" | "京"
2 | getStudent(2, "李四", "上海") || "200000" | "沪"
}
def getStudent(def id, def name, def province) {
return [new StudentDTO(id: id, name: name, province: province)]
}
where模块第一行代码是表格的列名,多个列使用|单竖线隔开,||双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:
输入参数1 | 输入参数2 || 输出结果1 | 输出结果2
而且IntelliJ IDEA支持format格式化快捷键,因为表格列的长度不一样,手动对齐比较麻烦。表格的每一行代表一个测试用例,即被测方法执行了2次,每次的输入和输出都不一样,刚好可以覆盖全部分支情况。比如id、students都是输入条件,其中students对象的构造调用了getStudent方法,每次测试业务代码传入不同的student值,postCodeResult、abbreviationResult表示对返回的response对象的属性判断是否正确。第一行数据的作用是验证返回的邮编是否是100000,第二行是验证邮编是否是200000。这个就是where+with的用法,更符合我们实际测试的场景,既能覆盖多种分支,又可以对复杂对象的属性进行验证,其中在定义的测试方法名,使用了Groovy的字面值特性:
即把请求参数值和返回结果值的字符串动态替换掉,#id、#postCodeResult、#abbreviationResult#号后面的变量是在方法内部定义的,实现占位符的功能。
@Unroll注解,可以把每一次调用作为一个单独的测试用例运行,这样运行后的单元测试结果更加直观:
而且如果其中某行测试结果不对,Spock的错误提示信息也很详细,方便进行排查(比如我们把第1条测试用例返回的邮编改成100001):
可以看出,第1条测试用例失败,错误信息是postCodeResult的预期结果和实际结果不符,业务代码逻辑返回的邮编是100000,而我们预期的邮编是100001,这样就可以排查是业务代码逻辑有问题,还是我们的断言不对。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理