美团优选实践:Spock单元测试框架介绍及应用(三)

发表于:2021-9-01 09:38  作者:美团技术团队   来源:掘金

字体: | 上一篇 | 下一篇 |我要投稿 | 推荐标签: 软件测试技术 单元测试

  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),我们将立即处理

评 论

论坛新帖



建议使用IE 6.0以上浏览器,800×600以上分辨率,法律顾问:上海信义律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2021, 沪ICP备05003035号
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪公网安备 31010102002173号

51Testing官方微信

51Testing官方微博

扫一扫 测试知识全知道