首页 » 网站建设 » cnyconvertphp技巧_Spock单元测试框架介绍以及在美团优选的实践

cnyconvertphp技巧_Spock单元测试框架介绍以及在美团优选的实践

duote123 2024-12-02 0

扫一扫用手机浏览

文章目录 [+]

只管单元测试有如此的收益,但在我们日常的事情中,仍旧存在不少项目它们的单元测试要么是不完全要么是缺失落的。
常见的缘故原由总结如下:代码逻辑过于繁芜;写单元测试时耗费的韶光较长;任务重、工期紧,或者干脆就不写了。

基于以上问题,相较于传统的JUnit单元测试,本日为大家推举一款名为Spock的测试框架。
目前,美团优选物流技能团队绝大部分后端做事已经采取了Spock作为测试框架,在开拓效率、可读性和掩护性方面取得了不错的收益。

cnyconvertphp技巧_Spock单元测试框架介绍以及在美团优选的实践

不过网上Spock资料比较大略,乃至包括官网的Demo,无法办理我们项目中繁芜业务场景面临的问题,通过深入学习和实践之后,本文会将一些履历分享出来,希望能够帮助大家提高开拓测试的效率。

cnyconvertphp技巧_Spock单元测试框架介绍以及在美团优选的实践
(图片来自网络侵删)
2. Spock是什么?和JUnit、jMock有什么差异?

Spock是一款国外精良的测试框架,基于BDD(行为驱动开拓)思想实现,功能非常强大。
Spock结合Groovy动态措辞的特点,供应了各种标签,并采取大略、通用、构造化的描述措辞,让编写测试代码更加简洁、高效。
官方的先容如下:

What is it? Spock is a testing and specification framework for Java and Groovy applications. What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, RSpec, jMock, Mockito, Groovy, Scala, Vulcans, and other fascinating life forms.

Spock是一个Java和Groovy`运用的测试和规范框架。
之以是能够在浩瀚测试框架中脱颖而出,是由于它幽美而富有表现力的规范措辞。
Spock的灵感来自JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans。

大略来讲,Spock紧张特点如下:

让测试代码更规范,内置多种标签来规范单元测试代码的语义,测试代码构造清晰,更具可读性,降落后期掩护难度。
供应多种标签,比如:given、when、then、expect、where、with、thrown......帮助我们应对繁芜的测试场景。
利用Groovy这种动态措辞来编写测试代码,可以让我们编写的测试代码更简洁,适宜敏捷开拓,提高编写单元测试代码的效率。
屈服BDD(行为驱动开拓)模式,有助于提升代码的质量。
IDE兼容性好,自带Mock功能。
为什么利用Spock? Spock和JUnit、jMock、Mockito的差异在哪里?

总的来说,JUnit、jMock、Mockito都是相对独立的工具,只是针对不同的业务场景供应特定的办理方案。
个中JUnit纯挚用于测试,并不供应Mock功能。

我们的做事大部分是分布衰落做事架构。
做事与做事之间常日都是通过接口的办法进行交互。
纵然在同一个做事内也会分为多个模块,业务功能须要依赖下贱接口的返回数据,才能连续后面的处理流程。
这里的下贱不限于接口,还包括中间件数据存储比如Squirrel、DB、MCC配置中央等等,以是如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。
由于如果下贱接口不稳定可能会影响我们代码的测试结果,让下贱接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否精确,是否符合逻辑结果的预期。

只管jMock、Mockito供应了Mock功能,可以把接口等依赖屏蔽掉,但不能对静态方法Mock。
虽然PowerMock、jMockit能够供应静态方法的Mock,但它们之间也须要合营(JUnit + Mockito PowerMock)利用,并且语法上比较繁琐。
工具多了就会导致不同的人写出的单元测试代码“五花八门”,风格相差较大。

Spock通过供应规范性的描述,定义多种标签(given、when、then、where等),去描述代码“该当做什么”,“输入条件是什么”,“输出是否符合预期”,从语义层面规范了代码的编写。

Spock自带Mock功能,利用大略方便(也支持扩展其他Mock框架,比如PowerMock),再加上Groovy动态措辞的强大语法,能写出简洁高效的测试代码,同时能方便直不雅观地验证业务代码的行为流转,增强工程师对代码实行逻辑的可控性。

3. 利用Spock办理单元测试开拓中的痛点

如果在(if/else)分支很多的繁芜场景下,编写单元测试代码的本钱会变得非常高,正常的业务代码可能只有几十行,但为了测试这个功能覆盖大部分的分支场景,编写的测试代码可能远不止几十行。

之前有碰着过某个功能上线良久一贯都很正常,没有涌现干涉干与题,但后来有个调用要求的数据不一样,走到了代码中一个不常用的逻辑分支时,涌现了Bug。
当时写这段代码的同学也认为只有很小几率才能走到这个分支,只管当时写了单元测试,但由于韶光比较紧张,分支又多,就漏掉了这个分支的测试。

只管利用JUnit的@Parametered参数化表明或者DataProvider办法可以办理多数据分支问题,但不足直不雅观,而且如果个中某一次分支测试Case出错了,它的报错信息也不足详尽。

这就须要一种编写测试用例高效、可读性强、占用工时少、掩护本钱低的测试框架。
首先不能让业务职员排斥编写单元测试,更不能让工程师以为写单元测试是在摧残浪费蹂躏韶光。
而且利用JUnit做测试事情量不算小。
据初步统计,采取JUnit的话,它的测试代码行和业务代码行能到3:1。
如果采取Spock作为测试框架的话,它的比例可缩减到1:1,能够大大提高编写测试用例的效率。

下面借用《编程珠玑》中一个打算税金的例子。

public double calc(double income) { BigDecimal tax; BigDecimal salary = BigDecimal.valueOf(income); if (income <= 0) { return 0; } if (income > 0 && income <= 3000) { BigDecimal taxLevel = BigDecimal.valueOf(0.03); tax = salary.multiply(taxLevel); } else if (income > 3000 && income <= 12000) { BigDecimal taxLevel = BigDecimal.valueOf(0.1); BigDecimal base = BigDecimal.valueOf(210); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 12000 && income <= 25000) { BigDecimal taxLevel = BigDecimal.valueOf(0.2); BigDecimal base = BigDecimal.valueOf(1410); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 25000 && income <= 35000) { BigDecimal taxLevel = BigDecimal.valueOf(0.25); BigDecimal base = BigDecimal.valueOf(2660); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 35000 && income <= 55000) { BigDecimal taxLevel = BigDecimal.valueOf(0.3); BigDecimal base = BigDecimal.valueOf(4410); tax = salary.multiply(taxLevel).subtract(base); } else if (income > 55000 && income <= 80000) { BigDecimal taxLevel = BigDecimal.valueOf(0.35); BigDecimal base = BigDecimal.valueOf(7160); tax = salary.multiply(taxLevel).subtract(base); } else { BigDecimal taxLevel = BigDecimal.valueOf(0.45); BigDecimal base = BigDecimal.valueOf(15160); tax = salary.multiply(taxLevel).subtract(base); } return tax.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); }

能够看到上面的代码中有大量的if-else语句,Spock供应了where标签,可以让我们通过表格的办法来测试多种分支。

@Unrolldef "个税打算,收入:#income, 个税:#result"() { expect: "when + then 的组合" CalculateTaxUtils.calc(income) == result where: "表格办法测试不同的分支逻辑" income || result -1 || 0 0 || 0 2999 || 89.97 3000 || 90.0 3001 || 90.1 11999 || 989.9 12000 || 990.0 12001 || 990.2 24999 || 3589.8 25000 || 3590.0 25001 || 3590.25 34999 || 6089.75 35000 || 6090.0 35001 || 6090.3 54999 || 12089.7 55000 || 12090 55001 || 12090.35 79999 || 20839.65 80000 || 20840.0 80001 || 20840.45}

上图中左边利用Spock写的单元测试代码,语法简洁,表格办法测试覆盖分支场景更加直不雅观,开拓效率高,更适宜敏捷开拓。

单元测试代码的可读性和后期掩护

我们微做事场景很多时候须要依赖其他接口返回的结果,才能验证自己的代码逻辑。
Mock工具是必不可少的。
但jMock、Mockito的语法比较繁琐,再加上单元测试代码不像业务代码那么直不雅观,又不能完备按照业务流程的思路写单元测试,这就让不少同学对单元测试代码可读性不足重视,终极导致测试代码难以阅读,掩护起来更是难上加难。
乃至很多同学自己写的单元测试,过几天再看也一样以为“云里雾里”的。
也有改了原来的代码逻辑导致单元测试实行失落败的;或者新增了分支逻辑,单元测试没有覆盖到的;终极随着业务的快速迭代单元测试代码越来越难以掩护。

Spock供应多种语义标签,如:given、when、then、expect、where、with、and等,从行为上规范了单元测试代码,每一种标签对应一种语义,让单元测试代码构造具有层次感,功能模块划分更加清晰,也便于后期的掩护。

Spock自带Mock功能,利用上大略方便(Spock也支持扩展第三方Mock框架,比如PowerMock)。
我们可以再看一个样例,对付如下的代码逻辑进行单元测试:

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; }

比较明显,左边的JUnit单元测试代码冗余,短缺构造层次,可读性差,随着后续的迭代,势必会导致代码的堆积,掩护本钱会变得越来越高。
右边的单元测试代码Spock会逼迫哀求利用given、when、then这样的语义标签(至少一个),否则编译不通过,这样就能担保代码更加规范,构造模块化,边界范围清晰,可读性强,便于扩展和掩护。
而且利用了自然措辞描述测试步骤,让非技能职员也能看懂测试代码(given表示输入条件,when触发动作,then验证输出结果)。

Spock自带的Mock语法也非常大略:dao.getStudentInfo() >> [student1, student2]。

两个右箭头>>表示仿照getStudentInfo接口的返回结果,再加上利用的Groovy措辞,可以直策应用[]中括号表示返回的是List类型。

单元测试不仅仅是为了统计代码覆盖率,更主要的是验证业务代码的健壮性、业务逻辑的严谨性以及设计的合理性

在项目初期阶段,可能为了追赶进度而没有韶光写单元测试,或者这个期间写的单元测试只是为了达到覆盖率的哀求(比如为了知足新增代码行或者分支覆盖率统计哀求)。

很多工程师写的单元测试基本都是采取Java这种强类型措辞编写,各种底层接口的Mock写起来不仅繁琐而且耗时。
这时的单元测试代码可能就写得比较粗糙,有粒度过大的,也有缺少单元测试结果验证的。
这样的单元测试对代码的质量帮助不大,更多是为了测试而测试。
末了韶光没少花,可效果却没有达到。

针对有效测试用例方面,我们测试根本组件组开拓了一些检测工具(作为抓手),比如去扫描大家写的单元测试,检测单元测试的断言有效性等。
其余在结果校验方面,Spock表现也是十分精良的。
我们可以来看接下来的场景:void方法,没有返回结果,如何写测试这段代码的逻辑是否精确?

如何确保单元测试代码是否实行到了for循环里面的语句,循环里面的打折打算又是否精确呢?

public void calculatePrice(OrderVO order){ BigDecimal amount = BigDecimal.ZERO; for (SkuVO sku : order.getSkus()) { Integer skuId = sku.getSkuId(); BigDecimal skuPrice = sku.getSkuPrice(); BigDecimal discount = BigDecimal.valueOf(discountDao.getDiscount(skuId)); BigDecimal price = skuPrice discount; amount = amount.add(price); } order.setAmount(amount.setScale(2, BigDecimal.ROUND_HALF_DOWN)); }

如果用Spock写的话,就会方便很多,如下图所示:

这里,2 discountDao.getDiscount(_) >> 0.95 >> 0.8 在for循环中一共调用了2次,第一次返回结果0.95,第二次返回结果0.8,末了再进行验证,类似于JUnit中的Assert断言。

这样的收益还是比较明显的,不仅提高了单元测试的可控性,而且方便验证业务代码的逻辑精确性和合理性,这也是BDD思想的一种表示。

4. Mock仿照

考虑如了局景,代码如下:

@Servicepublic 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,这样就可以排查是业务代码逻辑有问题,还是我们的断言不对。

5. 非常测试

我们再看下非常方面的测试,例如下面的代码:

public void validateStudent(StudentVO student) throws BusinessException { if(student == null){ throw new BusinessException("10001", "student is null"); } if(StringUtils.isBlank(student.getName())){ throw new BusinessException("10002", "student name is null"); } if(student.getAge() == null){ throw new BusinessException("10003", "student age is null"); } if(StringUtils.isBlank(student.getTelephone())){ throw new BusinessException("10004", "student telephone is null"); } if(StringUtils.isBlank(student.getSex())){ throw new BusinessException("10005", "student sex is null"); } }

BusinessException是封装的业务非常,紧张包含code、message属性:

/ 自定义业务非常 /public class BusinessException extends RuntimeException { private String code; private String message; setXxx... getXxx...}

这个大家该当都很熟习,针对这种抛出多个不同缺点码和缺点信息的非常。
如果利用JUnit的办法测试,会比较麻烦。
如果是单个非常还好,如果是多个的话,测试代码就不太好写。

@Test public void testException() { StudentVO student = null; try { service.validateStudent(student); } catch (BusinessException e) { Assert.assertEquals(e.getCode(), "10001"); Assert.assertEquals(e.getMessage(), "student is null"); } student = new StudentVO(); try { service.validateStudent(student); } catch (BusinessException e) { Assert.assertEquals(e.getCode(), "10002"); Assert.assertEquals(e.getMessage(), "student name is null"); } }

当然可以利用JUnit的ExpectedException办法:

@Rulepublic ExpectedException exception = ExpectedException.none();exception.expect(BusinessException.class); // 验证非常类型exception.expectMessage("xxxxxx"); //验证非常信息

或者利用@Test(expected = BusinessException.class) 表明,但这两种办法都有缺陷。

@Test办法不能指定断言的非常属性,比如code、message。
ExpectedException的办法也只供应了expectMessage的API,对自定义的code不支持,尤其像上面的有很多分支抛出多种不同非常码的情形。
接下来我们看下Spock是如何办理的。
Spock内置thrown()方法,可以捕获调用业务代码抛出的预期非常并验证,再结合where表格的功能,可以很方便地覆盖多种自定义业务非常,代码如下:

@Unroll def "validate student info: #expectedMessage"() { when: "校验" tester.validateStudent(student) then: "验证" def exception = thrown(expectedException) exception.code == expectedCode exception.message == expectedMessage where: "测试数据" student || expectedException | expectedCode | expectedMessage getStudent(10001) || BusinessException | "10001" | "student is null" getStudent(10002) || BusinessException | "10002" | "student name is null" getStudent(10003) || BusinessException | "10003" | "student age is null" getStudent(10004) || BusinessException | "10004" | "student telephone is null" getStudent(10005) || BusinessException | "10005" | "student sex is null" } def getStudent(code) { def student = new StudentVO() def condition1 = { student.name = "张三" } def condition2 = { student.age = 20 } def condition3 = { student.telephone = "12345678901" } def condition4 = { student.sex = "男" } switch (code) { case 10001: student = null break case 10002: student = new StudentVO() break case 10003: condition1() break case 10004: condition1() condition2() break case 10005: condition1() condition2() condition3() break } return student }

在then标签里用到了Spock的thrown()方法,这个方法可以捕获我们要测试的业务代码里抛出的非常。
thrown()方法的入参expectedException,是我们自己定义的非常变量,这个变量放在where标签里就可以实现验证多种非常情形的功能(Intellij Idea格式化快捷键,可以自动对齐表格)。
expectedException类型调用validateUser方法里定义的BusinessException非常,可以验证它所有的属性,code、message是否符合预期值。

6. Spock静态方法测试

接下来,我们一起看下Spock如何扩展第三方PowerMock对静态方法进行测试。

Spock的单元测试代码继续自Specification基类,而Specification又是基于JUnit的表明@RunWith()实现的,代码如下:

PowerMock的PowerMockRunner也是继续自JUnit,以是利用PowerMock的@PowerMockRunnerDelegate()表明,可以指定Spock的父类Sputnik去代理运行PowerMock,这样就可以在Spock里利用PowerMock去仿照静态方法、final方法、私有方法等。
实在Spock自带的GroovyMock可以对Groovy文件的静态方法Mock,但对Java代码支持不完全,只能Mock当前Java类的静态方法,官方给出的阐明如下:

如下代码:

public StudentVO getStudentByIdStatic(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()); // 静态方法调用 String abbreviation = AbbreviationProvinceUtil.convert2Abbreviation(studentDTO.getProvince()); studentVO.setAbbreviation(abbreviation); studentVO.setPostCode(studentDTO.getPostCode()); return studentVO; }

上面利用了AbbreviationProvinceUtil.convert2Abbreviation()静态方法,对应的测试用例代码如下:

@RunWith(PowerMockRunner.class)@PowerMockRunnerDelegate(Sputnik.class)@PrepareForTest([AbbreviationProvinceUtil.class])@SuppressStaticInitializationFor(["example.com.AbbreviationProvinceUtil"])class StudentServiceStaticSpec extends Specification { def studentDao = Mock(StudentDao) def tester = new StudentService(studentDao: studentDao) void setup() { // mock静态类 PowerMockito.mockStatic(AbbreviationProvinceUtil.class) } def "test getStudentByIdStatic"() { given: "创建工具" def student1 = new StudentDTO(id: 1, name: "张三", province: "北京") def student2 = new StudentDTO(id: 2, name: "李四", province: "上海") and: "Mock掉接口返回的学生信息" studentDao.getStudentInfo() >> [student1, student2] and: "Mock静态方法返回值" PowerMockito.when(AbbreviationProvinceUtil.convert2Abbreviation(Mockito.any())).thenReturn(abbreviationResult) when: "调用获取学生信息方法" def response = tester.getStudentByIdStatic(id) then: "验证返回结果是否符合预期值" with(response) { abbreviation == abbreviationResult } where: id || abbreviationResult 1 || "京" 2 || "沪" }}

在StudentServiceStaticSpec类的头部利用@PowerMockRunnerDelegate(Sputnik.class)表明,交给Spock代理实行,这样既可以利用Spock +Groovy的各种功能,又可以利用PowerMock的对静态,final等方法的Mock。
@SuppressStaticInitializationFor(["example.com.AbbreviationProvinceUtil"]),这行代码的浸染是限定AbbreviationProvinceUtil类里的静态代码块初始化,由于AbbreviationProvinceUtil类在第一次调用时可能会加载一些本地资源配置,以是可以利用PowerMock禁止初始化。
然后在setup()方法里对静态类进行Mock设置,PowerMockito.mockStatic(AbbreviationProvinceUtil.class)。
末了在test getStudentByIdStatic测试方法里对convert2Abbreviation()方法指定返回默认值:PowerMockito.when(AbbreviationProvinceUtil.convert2Abbreviation(Mockito.any())).thenReturn(abbreviationResult)。

运行时在掌握台会输出:

Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST

这是Powermock的警告信息,不影响运行结果。

如果单元测试代码不须要对静态方法、final方法Mock,就没必要利用PowerMock,利用Spock自带的Mock()就足够了。
由于PowerMock的事理是在编译期通过ASM字节码修正工具修正代码,然后利用自己的ClassLoader加载,而加载的静态方法越多,测试耗时就会越长。

7. 动态Mock静态方法

考虑场景,让静态方法每次调用返回不同的值。

以下代码:

public List<OrderVO> getOrdersBySource(){ List<OrderVO> orderList = new ArrayList<>(); OrderVO order = new OrderVO(); if ("APP".equals(HttpContextUtils.getCurrentSource())) { if("CNY".equals(HttpContextUtils.getCurrentCurrency())){ System.out.println("source -> APP, currency -> CNY"); } else { System.out.println("source -> APP, currency -> !CNY"); } order.setType(1); } else if ("WAP".equals(HttpContextUtils.getCurrentSource())) { System.out.println("source -> WAP"); order.setType(2); } else if ("ONLINE".equals(HttpContextUtils.getCurrentSource())) { System.out.println("source -> ONLINE"); order.setType(3); } orderList.add(order); return orderList;}

这段代码的if else分支逻辑,紧张是依据HttpContextUtils这个工具类的静态方法getCurrentSource()和getCurrentCurrency()的返回值来决定流程。
这样的业务代码也是我们平时写单元测试时常常碰着的场景,如果能让HttpContextUtils.getCurrentSource()静态方法每次Mock出不同的值,就可以很方便地覆盖if else的全部分支逻辑。
Spock的where标签可以方便地和PowerMock结合利用,让PowerMock仿照的静态方法每次返回不同的值,代码如下:

PowerMock的thenReturn方法返回的值是source和currency等2个变量,不是详细的数据,这2个变量对应where标签里的前两列source|currency。
这样的写法,就可以在每次测试业务方法时,让HttpContextUtils.getCurrentSource()和HttpContextUtils.getCurrentCurrency()返回不同的来源和币种,就能轻松的覆盖if和else的分支代码。
即Spock利用where表格的办法让PowerMock具有了动态Mock的功能。
接下来,我们再看一下如何对付final变量进行Mock。

public List<OrderVO> convertOrders(List<OrderDTO> orders){ List<OrderVO> orderList = new ArrayList<>(); for (OrderDTO orderDTO : orders) { OrderVO orderVO = OrderMapper.INSTANCE.convert(orderDTO); if (1 == orderVO.getType()) { orderVO.setOrderDesc("App端订单"); } else if(2 == orderVO.getType()) { orderVO.setOrderDesc("H5端订单"); } else if(3 == orderVO.getType()) { orderVO.setOrderDesc("PC端订单"); } orderList.add(orderVO); } return orderList;}

这段代码里的for循环第一行调用了OrderMapper.INSTANCE.convert()转换方法,将orderDTO转换为orderVO,然后根据type值走不同的分支,而OrderMapper是一个接口,代码如下:

@Mapperpublic interface OrderMapper { // 纵然不用static final润色,接口里的变量默认也是静态、final的 static final OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class); @Mappings({}) OrderVO convert(OrderDTO requestDTO);}

INSTANCE是接口OrderMapper里定义的变量,接口里的变量默认都是static final的,以是我们要先把这个INSTANCE静态final变量Mock掉,这样才能调用它的方法convert()返回我们想要的值。
OrderMapper这个接口是mapstruct工具的用法,mapstruct是做工具属性映射的一个工具,它会自动天生OrderMapper接口的实现类,天生对应的set、get方法,把orderDTO的属性值赋给orderVO属性,常日情形下会比利用反射的办法好不少。
看下Spock如何写这个单元测试:

@Unrolldef "test convertOrders"() { given: "Mock掉OrderMapper的静态final变量INSTANCE,并结合Spock设置动态返回值" def orderMapper = Mock(OrderMapper.class) Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper) orderMapper.convert(_) >> order when: def orders = service.convertOrders([new OrderDTO()]) then: "验证结果" with(orders) { it[0].orderDesc == desc } where: "测试数据" order || desc new OrderVO(type: 1) || "App端订单" new OrderVO(type: 2) || "H5端订单" new OrderVO(type: 3) || "PC端订单"}首先利用Spock自带的Mock()方法,将OrderMapper类Mock为一个仿照工具orderMapper,def orderMapper = Mock(OrderMapper.class)。
然后利用PowerMock的Whitebox.setInternalState(),对OrderMapper接口的static final常量INSTANCE赋值(Spock不支持静态常量的Mock),赋的值正是利用SpockMock的工具orderMapper。
利用Spock的Mock仿照convert()方法调用,orderMapper.convert(_) >> order,再结合where表格,实现动态Mock接口的功能。

紧张是这3行代码:

def orderMapper = Mock(OrderMapper.class) // 先利用Spock的MockWhitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper) // 通过PowerMock把Mock工具orderMapper赋值给静态常量INSTANCEorderMapper.convert(_) >> order // 结合where仿照不同的返回值

这样就可以利用Spock结合PowerMock测试静态常量,达到覆盖if else不同分支逻辑的功能。

8. 覆盖率

Jacoco是统计单元测试覆盖率的一种工具,当然Spock也自带了覆盖率统计的功能,这里利用第三方Jacoco的缘故原由紧张是海内公司利用的比较多一些,包括美团很多技能团队现在利用的也是Jacoco,所以为了兼容就以Jacoco来查看单元测试覆盖率。
这里说下如何通过Jacoco确认分支是否完备覆盖到。

在pom文件里引用Jacoco的插件:jacoco-maven-plugin,然后实行mvn package 命令,成功后会在target目录下天生单元测试覆盖率的报告,点开报告找到对应的被测试类查看覆盖情形。

绿色背景表示完备覆盖,黄色是部分覆盖,赤色没有覆盖到。
比如第34行黄色背景的else if() 判断,提示有二分之一的分支缺失落,虽然它下面的代码也被覆盖了(显示为绿色),这种情形跟详细利用哪种单元测试框架没紧要,由于这只是分支覆盖率统计的规则,只不过利用Spock的话,办理起来会更大略,只需在where下增加一行针对的测试数据即可。

9. DAO层测试

DAO层的测试有些不太一样,不能再利用Mock,否则无法验证SQL是否精确。
对付DAO测试有一样平常最简的办法是直策应用@SpringBootTest表明启动测试环境,通过Spring创建Mybatis、Mapper实例,但这种办法并不属于单元测试,而是集成测试范畴了,由于当启用@SpringBootTest时,会把全体运用的高下文加载进来。
不仅耗时时间长,而且一旦依赖环境上有任何问题,可能会影响启动,进而影响DAO层的测试。
末了,须要到数据库尽可能隔离,由于如果大家都利用同一个Test环境的数据的话,一旦测试用例编写有问题,就可能会污染Test环境的数据。

针对以上场景,可采取以下方案:

通过MyBatis的SqlSession启动mapper实例(避免通过Spring启动加载高下文信息)。
通过内存数据库(如H2)隔离大家的数据库连接(完备隔离不会存在相互关扰的征象)。
通过DBUnit工具,用为难刁难于数据库层的操作访问工具。
通过扩展Spock的表明,供应对付数据库Schema创建和数据Data加载的办法。
如csv、xml或直接Closure编写等。

在pom文件增加相应的依赖。

<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <scope>test</scope> </dependency> <dependency> <groupId>org.dbunit</groupId> <artifactId>dbunit</artifactId> <version>2.5.1</version> <scope>test</scope> </dependency>

增加Groovy的maven插件、资源文件拷贝以及测试覆盖率统计插件。

<!-- 测试插件 --><plugin> <groupId>org.codehaus.gmavenplus</groupId> <artifactId>gmavenplus-plugin</artifactId> <version>1.8.1</version> <executions> <execution> <goals> <goal>addSources</goal> <goal>addTestSources</goal> <goal>generateStubs</goal> <goal>compile</goal> <goal>generateTestStubs</goal> <goal>compileTests</goal> <goal>removeStubs</goal> <goal>removeTestStubs</goal> </goals> </execution> </executions></plugin><plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M3</version> <configuration> <useFile>false</useFile> <includes> <include>/Spec.java</include> </includes> <parallel>methods</parallel> <threadCount>10</threadCount> <testFailureIgnore>true</testFailureIgnore> </configuration></plugin><plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>2.6</version> <executions> <execution> <id>copy-resources</id> <phase>compile</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${basedir}/target/resources</outputDirectory> <resources> <resource> <directory>${basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </configuration> </execution> </executions></plugin><plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.2</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>prepare-package</phase> <goals> <goal>report</goal> </goals> </execution> <execution> <id>post-unit-test</id> <phase>test</phase> <goals> <goal>report</goal> </goals> <configuration> <dataFile>target/jacoco.exec</dataFile> <outputDirectory>target/jacoco-ut</outputDirectory> </configuration> </execution> </executions></plugin>

加入对付Spock扩展的自动处理框架(用于数据Schema和Data初始化操作)。

这里先容一下紧张内容,表明@MyDbUnit:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@ExtensionAnnotation(MyDbUnitExtension.class)@interface MyDbUnit { / <pre> content = { your_table_name(id: 1, name: 'xxx', age: 21) your_table_name(id: 2, name: 'xxx', age: 22) }) </pre> @return / Class<? extends Closure> content() default Closure.class; / xml存放路径(相对付测试类) @return / String xmlLocation() default ""; / csv存放路径(相对付测试类) @return / String csvLocation() default "";}

考虑以下代码的测试:

@Repository("personInfoMapper")public interface PersonInfoMapper { @Delete("delete from person_info where id=#{id}") int deleteById(Long id); @Select("select count() from person_info") int count(); @Select("select from user_info") List<PersonInfoDO> selectAll();}

Demo1 (利用@MyDbUnit,content指定导入数据内容,格式Closure)。

class Demo1Spec extends MyBaseSpec { / 直接获取待测试的mapper / def personInfoMapper = MapperUtil.getMapper(PersonInfoMapper.class) / 测试数据准备,常日为sql表构造创建用的ddl,支持多个文件以逗号分隔。
/ def setup() { executeSqlScriptFile("com/xxx/xxx/xxx/......../schema.sql") } / 数据表打消,常日待drop的数据表 / def cleanup() { dropTables("person_info") } / 直接布局数据库中的数据表,此方法适用于数据量较小的mapper sql测试 / @MyDbUnit( content = { person_info(id: 1, name: "abc", age: 21) person_info(id: 2, name: "bcd", age: 22) person_info(id: 3, name: "cde", age: 23) } ) def "demo1_01"() { when: int beforeCount = personInfoMapper.count() // groovy sql用于快速实行sql,不仅能验证数据结果,也可向数据中添加数据。
def result = new Sql(dataSource).firstRow("select from `person_info`") int deleteCount = personInfoMapper.deleteById(1L) int afterCount = personInfoMapper.count() then: beforeCount == 3 result.name == "abc" deleteCount == 1 afterCount == 2 } / 直接布局数据库中的数据表,此方法适用于数据量较小的mapper sql测试 / @MyDbUnit(content = { person_info(id: 1, name: 'a', age: 21) }) def "demo1_02"() { when: int beforeCount = personInfoMapper.count() def result = new Sql(dataSource).firstRow("select from `person_info`") int deleteCount = personInfoMapper.deleteById(1L) int afterCount = personInfoMapper.count() then: beforeCount == 1 result.name == "a" deleteCount == 1 afterCount == 0 }}

在setup()阶段,把数据库表中的Schema创建好,然后通过下面的@MyDbUnit表明的content属性,把数据导入到数据库中。
person_info是表名,id、name、age是数据。

通过MapperUtil.getMapper()方法获取mapper实例。

当测试数据量较大时,可以编写相应的数据文件,通过@MyDbUnit的xmlLocation或csvLocation加载文件(分别支持csv和xml格式)。

如通过csv加载文件,csvLocation指向csv文件所在文件夹。

@MyDbUnit(csvLocation = "com/xxx/........./data01") def "demo2_01"() { when: int beforeCount = personInfoMapper.count() def result = new Sql(dataSource).firstRow("select from `person_info`") int deleteCount = personInfoMapper.deleteById(1L) int afterCount = personInfoMapper.count() then: beforeCount == 3 result.name == "abc" deleteCount == 1 afterCount == 2 }

通过xml加载文件,xmlLocation指向xml文件所在路径。

@MyDbUnit(xmlLocation = "com/xxxx/........./demo3_02.xml") def "demo3_02"() { when: int beforeCount = personInfoMapper.count() def result = new Sql(dataSource).firstRow("select from `person_info`") int deleteCount = personInfoMapper.deleteById(1L) int afterCount = personInfoMapper.count() then: beforeCount == 1 result.name == "a" deleteCount == 1 afterCount == 0 }

还可以不通过@MyDbUnit而利用API直接加载测试数据文件。

class Demo4Spec extends MyBaseSpec { def personInfoMapper = MapperUtil.getMapper(PersonInfoMapper.class) / 数据表打消,常日待drop的数据表 / def cleanup() { dropTables("person_info") } def "demo4_01"() { given: executeSqlScriptFile("com/xxxx/.........../schema.sql") IDataSet dataSet = MyDbUnitUtil.loadCsv("com/xxxx/.........../data01"); DatabaseOperation.CLEAN_INSERT.execute(MyIDatabaseConnection.getInstance().getConnection(), dataSet); when: int beforeCount = personInfoMapper.count() def result = new Sql(dataSource).firstRow("select from `person_info`") int deleteCount = personInfoMapper.deleteById(1L) int afterCount = personInfoMapper.count() then: beforeCount == 3 result.name == "abc" deleteCount == 1 afterCount == 2 } def "demo4_02"() { given: executeSqlScriptFile("com/xxxx/.........../schema.sq") IDataSet dataSet = MyDbUnitUtil.loadXml("com/xxxx/.........../demo3_02.xml"); DatabaseOperation.CLEAN_INSERT.execute(MyIDatabaseConnection.getInstance().getConnection(), dataSet); when: int beforeCount = personInfoMapper.count() def result = new Sql(dataSource).firstRow("select from `person_info`") int deleteCount = personInfoMapper.deleteById(1L) int afterCount = personInfoMapper.count() then: beforeCount == 1 result.name == "a" deleteCount == 1 afterCount == 0 }}作者简介

建华,美团优选奇迹部工程师。

| 本文系美团技能团队出品,著作权归属美团。
欢迎出于分享和互换等非商业目的转载或利用本文内容,敬请注明“内容转载自美团技能团队”。
本文未经容许,不得进行商业性转载或者利用。
任何商用行为,请发送邮件至tech@meituan.com申请授权。

标签:

相关文章

语言运用惰性,从现状到应对步骤

语言是人类沟通的桥梁,是文化传承的载体。在现代社会,随着科技的发展,人们越来越依赖智能设备和网络,导致语言运用惰性现象日益严重。本...

网站建设 2024-12-29 阅读0 评论0

语言领域指标,多维视角下的语言发展研究

语言是人类文明的载体,是沟通交流的工具。随着科技的飞速发展,语言领域的研究日益深入,语言领域指标成为衡量语言发展水平的重要标准。本...

网站建设 2024-12-29 阅读0 评论0

语言风格的魅力与运用

语言是人类沟通交流的工具,而语言风格则是语言表达的一种特色。它不仅体现了说话者的个性和情感,还彰显了说话者的修养和智慧。本文将探讨...

网站建设 2024-12-29 阅读0 评论0

跨文化交际中的区域语言区别与融合

随着全球化进程的不断加快,跨文化交际已成为现代社会的重要特征。在跨文化交际过程中,语言作为一种重要的沟通工具,其区域差异成为了影响...

网站建设 2024-12-29 阅读0 评论0