• 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.ISlaService
    import com.funtester.vo.sla.SlaBean
    import spock.lang.Shared
    import 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.ApiRMapper
    import com.funtester.vo.ApiR
    import spock.lang.Shared
    import spock.lang.Specification
    
    
        ApiRMapper mapper = Mock(ApiRMapper)
    
        def drive = new ApiImpl(mapper:mapper)
    
    

    PowerMock用法

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

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

    源代码如下:

    @Component
    @Slf4j
    public 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 !

  • 相关阅读:
    设计模式_抽象工厂模式
    KMeans聚类算法Hadoop实现
    JDK核心JAVA源代码解析(1)
    pushlet单播与多播
    SQL 2008 R2数据库变为REPLICATION,日志不断增长并且不能截断和收缩的解决方式
    chrome插件的popup与跨域请求
    Ubuntu vim+ ctags(包括系统函数) + taglist 配置
    spring Valid @Pattern 常见的验证表达式
    spring boot 全局异常处理
    spring 事件使用
  • 原文地址:https://www.cnblogs.com/FunTester/p/15817831.html
Copyright © 2020-2023  润新知