写点什么

Spock 框架 Mock 对象、方法经验总结

作者:FunTester
  • 2022 年 1 月 18 日
  • 本文字数:4127 字

    阅读完需:约 14 分钟

Spock框架Mock对象、方法经验总结

近期已然陷入了单元测试的汪洋大海,上万行的代码突然要求起来单元测试覆盖率,着实很恐怖的。最经过艰苦的抗争学习之后,终于迈过了技术这个坎儿,特来分享一下最近踩坑的经历,和一些典型的使用场景案例分享。


下面是我使用过的一个常用项目,部分信息隐去了。大家在自己项目中实践的时候可以参考,尽量别直接抄代码,我自己使用过程中有很多兼容性的坑,特别是 IDE 自动 import 功能。

技术方案

本技术方案基于公司力推的 Spock 单元测试框架,spock是一款基于 Groovy 语言的单元测试框架,其基础也是JavaJunit,目前最新版已经到了2.0,但对Groovy和相应的Java版本要求较高,所以 Groovy 版本使用 1.+,Spock 自带的 Mock 和 Spy 足够好了,对于对象行为的模拟满足绝大部分场景,但是涉及静态方法模拟时候存在局限性,所以引入 Mockito 和 PowerMock 来实现设计静态方法的测试模拟场景。


以下为相关依赖版本:


        <dependency>            <groupId>org.spockframework</groupId>            <artifactId>spock-core</artifactId>            <version>1.2-groovy-2.5</version>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.spockframework</groupId>            <artifactId>spock-spring</artifactId>            <version>1.2-groovy-2.5</version>            <scope>test</scope>        </dependency>
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>1.7.4</version> <scope>test</scope> </dependency>
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>1.7.4</version> <scope>test</scope> </dependency>
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.8.9</version> <scope>test</scope> </dependency>
复制代码


因为 Spock 本身需要 Groovy 语言支持,所以也需要一个 Groovy-all 的依赖,请注意注自带的注释:


        <dependency> <!-- use a specific Groovy version rather than the one specified by spock-core -->            <groupId>org.codehaus.groovy</groupId>            <artifactId>groovy-all</artifactId>            <version>2.4.7</version>        </dependency>
复制代码


另外,提供的配置文件中多了几项特殊场景下使用的依赖,提供参考:


        <dependency> <!-- enables mocking of classes (in addition to interfaces) -->            <groupId>net.bytebuddy</groupId>            <artifactId>byte-buddy</artifactId>            <version>1.9.9</version>            <scope>test</scope>        </dependency>        <dependency> <!-- enables mocking of classes without default constructor (together with CGLIB) -->            <groupId>org.objenesis</groupId>            <artifactId>objenesis</artifactId>            <version>3.0.1</version>            <scope>test</scope>        </dependency>        <dependency> <!-- only required if Hamcrest matchers are used -->            <groupId>org.hamcrest</groupId>            <artifactId>hamcrest-core</artifactId>            <version>2.1</version>            <scope>test</scope>        </dependency>
复制代码

非静态资源

由于多个单测框架的方法名重复较多,我把 import 内容也贴出来了,如果同样的代码无法运行,可以排查一下是否 import 正确的方法和类。这里不是很建议 import static ,因为可能出现混用以及不易排查的问题。


由于目前测试中没有遇到使用 Spy 放行的逻辑,所以均使用 Mock 模式,需要对 Mock 对象的方法进行模拟。这个分为两类:Spock 和 PowerMock(结合 Mockito)。原因是在混合静态资源和非静态资源场景下,指定了 PowerMock 的@RunWith运行规则,不兼容 Spock 写法,需要用到 PowerMock 框架 Mock 对象的功能。

Mock 被测对象

@Autowired 构造方法

用一个 controller 举例,源代码如下:


@Api(tags = "SLA规则管理模块")@Slf4j@RestController@RequestMapping("/hickwall/v1/static/sla")public class FunController {
HttpServletRequest request;
ISlaService service;
@Autowired public FunController(HttpServletRequest request, ISlaService service) { this.request = request; this.service = service; }
}
复制代码


Spock 单测代码如下:


import com.funtester.service.ISlaServiceimport com.funtester.vo.sla.SlaBeanimport spock.lang.Sharedimport spock.lang.Specification
import javax.servlet.http.HttpServletRequest
class FunControllerTest extends Specification {
def service = Mock(ISlaService)
@Shared def request = Mock(HttpServletRequest) def FunController = new FunController(request, service)
}
复制代码

@Autowired 属性对象,无构造方法

源代码如下:


public class ApiImpl implements IApi {
@Autowired private ApiRMapper mapper;}
复制代码


Spock 单测部分代码如下:


import com.funtester.mapper.ApiRMapperimport com.funtester.vo.ApiRimport spock.lang.Sharedimport spock.lang.Specification

ApiRMapper mapper = Mock(ApiRMapper)
def drive = new ApiImpl(mapper:mapper)
复制代码

PowerMock 用法

场景也分为两种:有无构造方法,除了 Mock 方法不同以外,其他均相同,这里不列举。


PS:如果对象属性中有未被@Autowired注释的属性,不能用@AllArgsConstructor的 lombok 注解,服务启动会报错。


源代码如下:


@Component@Slf4jpublic class TaskScheduled {
@Autowired IService service; @Value("${hickwall.statistic.cid}") public String cid;
}
复制代码

共享对象以及初始化

统一使用 Spock 提供的功能,用到的注解@Shared,不加的话无法在 Spock 方法中进行赋值操作,但是可以当做一个普通的对象使用。


Spock 框架 Demo:


    @Shared    def slaBean = new SlaBean()
def setupSpec() { request.getHeader("operator") >> "FunTester" slaBean.name = "测试" slaBean.quota = 1 slaBean.upstream = "PRO" slaBean.threshold = 0.1 slaBean.creator = "FunTester" slaBean.id = 100 }
复制代码

定义对象行为

Spock 定义 Mock 对象行为

基础的 Spock 语法结构when-then-expct如下:


    def "AddSla"() {        when:        def sla = FunController.addSla(slaBean)        then:        service.addSla(_) >> {f ->            assert "FunTester" in f.creator            1        }
expect:
sla.code == 0 sla.data == 1
}
复制代码


也可以在最开始加上givenwhenthen通常一起使用。


上述 Demo 在 Mock 方法的时候对参数进行了断言和处理,这也是 Spock 框架的一个特性,其他均为 Groovy 语法特性。


其他定义 Mock 行为的语法如下:


        service.getAllGroup(_,_) >> null//返回null        service.getAllGroup(_,_) >> {throw new Exception()} //抛出异常        service.getAllGroup(_,_) >> []//返回空list,Groovy默认实现ArrayList        service.getAllGroup(_,_) >> [slaBean,slaBean]//返回正常list        service.getAllGroup(_,_) >> [slaBean,slaBean]//返回正常list        service.getAllGroup(_,10) >> [slaBean,slaBean]//定时某个参数        service.getAllGroup(any(),10) >> [slaBean,slaBean]//any()等效于_        service.getAllGroup(any(),10) >> service.getAllGroup(1,10)//调用其他方法返回
复制代码

Mockito 模拟对象行为

Mockito 和 PowerMock 配合使用语法稍微复杂一些。首先我们需要先定义对象行为(通常在com.funtesterbase.task.TaskScheduledTest#setupSpec方法中),然后在用例用使用。


定时对象行为:


        Mockito.when(newutil.filter(Mockito.any())).thenReturn(true)
复制代码


定义行为以后,就可以在 Spock 用例中正常使用,包括在通过 Mock 对象创建的对象方法中,如果调用到定义过行为的方法,也会走自定义的逻辑。


其他常用定义行为:


        Mockito.when(newutil.filter(Mockito.any())).thenReturn(null)        Mockito.when(newutil.filter(Mockito.any())).thenThrow(Exception.class)//抛出异常        PowerMockito.doNothing().when(newutil).filter(Mockito.any(ArrayList.class))//dothing,什么都不做
复制代码


第三个例子中我们假设filter方法是一个无返回的void方法。


通常我们需要构建返回对象,如果对象需要赋值的属性过多,可以使用初始化赋值的方法,下面是 Mock 一个返回 list 的方法返回值的 Demo:


Mockito.when(newser.selectAllService()).thenReturn([new NewInterface() {
{ setUrl("/abc") setNname("test") setMethod("GET") } }, new NewInterface() {
{ setUrl("/abcd") setNname("test") setMethod("POST") } }, new NewInterface() {
{ setUrl("/abce") setNname("test") setMethod("GET") } }])
复制代码

Have Fun ~ Tester !

发布于: 刚刚阅读数: 2
用户头像

FunTester

关注

公众号:FunTester,750篇原创,欢迎关注 2020.10.20 加入

公众号FunTester,坚持原创文章的测试人,一个有趣的灵魂。

评论

发布
暂无评论
Spock框架Mock对象、方法经验总结