Spock单测利器,用了都说好

简介: 参考Spock单元测试框架介绍以及在美团优选的实践最近发现了一种写法简洁高效,一个单测方法可以测试多组测试数据,且测试结果一目了然的单测框架Spock。Spock国外的测试框架,其设计灵感来自JUnit、Mockito、Groovy,可以用于Java和Groovy应用的测试。尽管Spock写单测,需要使用groovy语言,但是groovy语言是一种弱类型,写法超级简单,我也是零基础的groovy新

参考

Spock单元测试框架介绍以及在美团优选的实践

最近发现了一种写法简洁高效,一个单测方法可以测试多组测试数据,且测试结果一目了然的单测框架Spock。Spock国外的测试框架,其设计灵感来自JUnit、Mockito、Groovy,可以用于Java和Groovy应用的测试。尽管Spock写单测,需要使用groovy语言,但是groovy语言是一种弱类型,写法超级简单,我也是零基础的groovy新手,相信你看过这篇文档,就会用groovy写单测啦。

环境配置

引入jar包

 			  <!--groovy单测框架-->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all-tests</artifactId>
            <version>2.0.0-rc-3</version>
        </dependency>
        <!-- Mandatory dependencies for using Spock test framework -->
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>1.3-groovy-2.4</version>
        </dependency>
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-spring</artifactId>
            <version>1.3-groovy-2.4</version>
            <scope>test</scope>
        </dependency>

配置插件

						<plugin>
                <!--groovy plugin-->
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>1.4</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                		<!-- spock单测文件路径 -->
                    <testSources>
                        <testSource>
                            <directory>${project.basedir}/src/test/java</directory>
                            <includes>
                                <include>**/*.groovy</include>
                            </includes>
                        </testSource>
                        <testSource>
                            <directory>${project.basedir}/src/test/groovy</directory>
                            <includes>
                                <include>**/*.groovy</include>
                            </includes>
                        </testSource>
                    </testSources>
                </configuration>
            </plugin>

Spock用法

0、被测试类

public class TaskService {
    /**
     * 环境
     */
    @Value("${spring.current.env}")
    private String env;

    /**
     * 任务 服务类
     */
    @Resource
    private ITaskRepository taskRepository;

    /**
     * 智能配置 管理类
     */
    @Resource
    private IntelligentConfigManager configManager;

    /**
     * 查询任务信息(根据环境,查询任务)
     *
     * @return 任务
     */
    public Result<Task> getTask() {
        if (EnvEnum.isDaily(env)) {
            // 日常环境,任务取值 本方法直接new
            Task task = new Task();
            task.setInput(EnvEnum.DAILY.name() + " 任务");
            return Result.isOk(task);
        }
        if (EnvEnum.isPre(env)) {
            // 预发环境,任务取值于 本类的方法的返回值
            return getPreTask();
        }
        try {
            // 线上环境,任务取值于 另一个类的方法的返回值
            return Result.isOk(taskRepository.getTask(1L));
        } catch (Exception ex) {
            // 异常
            return Result.onError("异常任务");
        }
    }

    /**
     * 查询智能配置信息
     *
     * @param query 查询智能配置信息的query
     * @return 智能配置信息集合
     */
    public Result<List<IntelligentConfigDTO>> getAllIntelligentConfigDTOList(IntelligentConfigQuery query) {
        List<IntelligentConfigDTO> allConfigDTOList = Lists.newArrayList();

        // 查询全部的智能配置信息
        query.setPage(1);
        PageResult<IntelligentConfigDTO> intelligentConfigDTOPageResult = configManager.queryList(query);
        while (intelligentConfigDTOPageResult.isSuccessful()
            && !CollectionUtils.isEmpty(intelligentConfigDTOPageResult.getList())) {
            allConfigDTOList.addAll(Lists.newArrayList(intelligentConfigDTOPageResult.getList()));

            query.setPage(query.getPage() + 1);
            intelligentConfigDTOPageResult = configManager.queryList(query);
        }
        return Result.isOk(allConfigDTOList);
    }

    /**
     * 查询预发环境的任务
     *
     * @return 任务
     */
    public Result<Task> getPreTask() {
        Task task = new Task();
        task.setInput("TaskService getInternalTask 任务" + EnvEnum.PRE.name());
        return Result.isOk(task);
    }
}

1、given-expect-where

given块:用于写测试前的准备工作,例如mock方法的返回值等

expect块:只能写判断式,如a==b

where块:用于写断言(测试数据、及期望返回值),where块可以写多组测试数据、和期望返回值

@Unroll:通过“#”可以动态获取where块的数据,测试结果一目了然,看例子,更容易理解

import com.google.common.collect.Lists
import spock.lang.Specification
import spock.lang.Unroll

class TaskServiceSpockTest extends Specification {

    // mock TaskService的属性
    ITaskRepository taskRepository = Mock()
    IntelligentConfigManager configManager = Mock()

    // 要测试的类
    TaskService taskService = new TaskService(taskRepository: taskRepository, configManager: configManager)

    void setup() {
        // 也可以在setup中,给TaskService的属性赋值
        // taskTestService.taskRepository = taskRepository
        // taskTestService.configManager = configManager
    }

    @Unroll
    def "testGetTask 环境=#env, 任务包含关键字=#keyWord, 任务是否包含关键字=#result"() {
        given: "测试前的准备:给taskService的env赋值"
        taskService.env = env

        and: "mock taskRepository.getTask(_) 的返回值"
        Task task = new Task();
        task.setInput(EnvEnum.PRODUCT.name())
        taskRepository.getTask(_) >> task

        and: "执行taskService.getTask()"
        Result<Task> taskResult = taskService.getTask()
        println(taskResult)

        expect: "expect只能写判断式,断言测试结果"
        result == taskResult.getData().getInput().contains(keyWord)

        where: "测试数据、及测试结果"
        env                      | keyWord                | result
        EnvEnum.DAILY.getVal()   | EnvEnum.DAILY.name()   | true
        EnvEnum.PRE.getVal()     | EnvEnum.PRE.name()     | true
        EnvEnum.PRODUCT.getVal() | EnvEnum.PRODUCT.name() | true
    }
}

执行结果

看到这里,你是不是很兴奋,一个单测就能覆盖到被测方法的各个逻辑,且测试结果一目了然。

2、given-when-then

除了given-expect-where,还可以使用given-when-then。

given块:用于写测试前的准备工作,例如mock方法的返回值等

when块:执行被测试方法

then块:用于写断言,如==、不会抛出异常noExceptionThrown()、方法被调用的次数0 * taskRepository.getTask(_)

    def "testGetTaskWhen"() {
        given: "测试前的准备: mock taskRepository.getTask(_)的返回值"
        Task task = new Task();
        task.setInput(EnvEnum.PRODUCT.name())
        taskRepository.getTask(_) >> task

        and: "给taskService的env赋值"
        taskService.env = EnvEnum.PRODUCT.getVal()

        when: "执行被测试方法"
        Result<Task> result = taskService.getTask()
        println(result)

        then: "断言"
        // 断言:返回结果是true
        result.isSuccessful() == true
        // 断言:不会抛出异常
        noExceptionThrown()
    }

3、mock异常

com.alibaba.polystar.service.intelligent.service.TaskService#getTask有try-catch,那么怎么覆盖掉catch的逻辑呢?下面讲下,如何mock异常。


    @Unroll
    def "testGetTaskException 环境=#env, 结果=#result"() {
        given: "测试前的准备:给taskService的env赋值"
        taskService.env = env

        and: "mock taskRepository.getTask(_) 抛出异常"
        taskRepository.getTask(_) >> { throw new Exception() }

        and: "执行被测试方法"
        Result<Task> result1 = taskService.getTask()
        println(result1)

        expect: "expect只能是判断式:断言 测试结果"
        result == result1.isSuccessful()

        // 测试数据、测试结果断言
        where:
        env                      | result
        EnvEnum.DAILY.getVal()   | true
        EnvEnum.PRE.getVal()     | true
        EnvEnum.PRODUCT.getVal() | false
    }

通过given-when-then测试抛出异常,不能像where能测试多组测试数据。

    def "testGetTaskWhen 异常"() {
        given: "测试前的准备: mock taskRepository.getTask(_)抛出运行时异常"
        taskRepository.getTask(_) >> { throw new RuntimeException() }

        and: "给taskService的env赋值"
        taskService.env = EnvEnum.PRODUCT.getVal()

        when: "执行被测试方法"
        Result<Task> result1 = taskService.getTask()
        println(result1)

        then: "断言测试结果"
        result1.isSuccessful() == false
    }

4、mock方法每次的返回值不一样

在日常开发中,可能会遇到while查询某个方法,直到某种条件,才会break,如TaskService.getAllIntelligentConfigDTOList。为了测试这样的逻辑,就需要使每次mock方法的返回值不同。

 def "testGetAllIntelligentConfigDTOList"() {

        given: "测试前的准备"

        // 第一次调,返回 长度=1的集合
        IntelligentConfigDTO configDTO = new IntelligentConfigDTO();
        configDTO.setId(1L)
        com.alibaba.polystar.common.PageResult<IntelligentConfigDTO> pageResult =
                PageResult.build(1, 1, 1, Lists.newArrayList(configDTO))

        // 第二次调,返回 空集合,使while循环结束
        com.alibaba.polystar.common.PageResult<IntelligentConfigDTO> pageResult2 =
                PageResult.build(2, 1, 0, Lists.newArrayList())

        // 模拟方法调多次时,返回的结果
        configManager.queryList(_) >> pageResult >> pageResult2

        // 执行被测试方法
        IntelligentConfigQuery query = new IntelligentConfigQuery();
        Result<List<IntelligentConfigDTO>> result = taskService.getAllIntelligentConfigDTOList(query)
        println(result)
        // 智能配置的总条数
        def size = result.getData().size()

        expect: "expect只能是判断式:断言 测试结果,断言智能配置size=1"
        size == 1
    }

5、mock本类方法

在日常开发中,被测试方法A调用了同类的方法B,而B方法逻辑复杂,如getPreTask()方法,会调用本类的getInternalTask(),这时可以通过spy来mock本类方法getInternalTask(),来编写getPreTask()方法的单测。TaskService taskService = Spy()的作用是,如果TaskService的方法没有mock的话,则会执行方法;如果TaskService的方法被mock的话,则不会执行方法。


    /**
     * 查询预发环境的任务
     *
     * @return 任务
     */
    public Result<Task> getPreTask() {
        return Result.isOk(getInternalTask());
    }

    /**
     * 查询 内部 任务
     *
     * @return 任务
     */
    public Task getInternalTask() {
        Task task = new Task();
        task.setInput("TaskService getInternalTask 任务" + EnvEnum.PRE.name());
        return task;
    }

单测


    def "testGetPreTask"() {
        given: "测试前的准备"
        // 通过spy创建TaskService,TaskService的方法如果没有mock的话,则会执行方法;如果TaskService的方法被mock的话,则不会执行方法
        TaskService taskService = Spy();

        and: "mock 本类的的方法"
        Task task = new Task();
        task.setInput("spy getInternalTask 任务");
        taskService.getInternalTask() >> task

        and: "执行被测试方法"
        Result<Task> result = taskService.getPreTask()
        println(result)

        expect: "expect只能是判断式:断言测试结果"
        result.getData().getInput().contains("spy") == true
    }

最后

看到这里,是不是你也觉得spock语法非常简洁、功能非常强大,那就快快使用起来吧。

相关文章
|
1月前
|
设计模式 关系型数据库 测试技术
进阶技巧:提高单元测试覆盖率与代码质量
【10月更文挑战第14天】随着软件复杂性的不断增加,确保代码质量的重要性日益凸显。单元测试作为软件开发过程中的一个重要环节,对于提高代码质量、减少bug以及加快开发速度都有着不可替代的作用。本文将探讨如何优化单元测试以达到更高的测试覆盖率,并确保代码质量。我们将从编写有效的测试用例策略入手,讨论如何避免常见的测试陷阱,使用mocking工具模拟依赖项,以及如何重构难以测试的代码。
56 4
|
3月前
|
测试技术 开发者
守护代码质量的利器:揭秘Vaadin单元测试的奥秘,助你打造无懈可击的Web应用
【8月更文挑战第31天】在软件开发中,单元测试是确保代码质量和稳定性的重要手段。对于使用Vaadin框架开发的Web应用,有效的单元测试尤为关键。Vaadin提供了完善的工具链支持,并鼓励测试驱动开发(TDD)。本文详细介绍了如何为Vaadin应用编写单元测试,并通过具体示例展示了测试环境搭建、依赖配置以及对简单`UserForm`组件的测试方法。通过JUnit和Mockito,我们验证了表单字段的变化及有效性,确保组件按预期工作,从而提升应用的整体健壮性和可靠性。这不仅有助于发现潜在问题,还能简化未来的维护工作。
40 0
|
4月前
|
测试技术 开发者
单元测试问题之为什么单测覆盖率高的项目模块更易于迭代演进
单元测试问题之为什么单测覆盖率高的项目模块更易于迭代演进
|
6月前
|
IDE 测试技术 持续交付
【专栏】利用Python自动化测试与单元测试框架提升代码质量与效率
【4月更文挑战第27天】本文探讨了Python自动化测试与单元测试框架在提升代码质量与效率中的作用。Selenium、Appium用于Web和移动应用自动化测试,pytest提供强大、易扩展的测试支持。unittest是Python标准的单元测试框架,支持结构化测试用例和丰富的断言。实践中,应制定测试计划,编写高质量测试用例,实行持续集成与测试,并充分利用测试报告。这些工具和策略能有效保障代码质量和提升开发效率。
59 1
|
测试技术 Linux Go
后端实践--单元测试与性能优化 青训营
后端实践--单元测试与性能优化 青训营
|
人工智能 自然语言处理 Java
提升函数代码质量的利器有哪些?
全栈式全自动软件开发工具SoFlu软件机器人结合当下AI技术今年重磅上线函数AI生成器——FuncGPT(慧函数)。FuncGPT(慧函数)采用代码编写最佳实践及大规模机器联合训练的方式,可以显著提高代码的质量、可维护性、健壮性,为中国软件开发者提供全栈式全流程软件开发的最佳体验。
|
Java 测试技术 Python
关于如何写自动化测试框架,看完本章你会有收获
关于如何写自动化测试框架,看完本章你会有收获
113 0
自动化测试技术笔记(三):如何编写技术方案
首先这个要求我觉得挺正常,一方面评审可以查漏补缺完善细节,另一方面也可以考察具体的落地经验和能力。其次,我认为技术方案其实有个通用的模版,或者说抽象的经验参考,这也是本篇文章我想聊的话题。
|
Web App开发 JavaScript Java
推荐一款自动化测试神器,不会写代码也能做!
推荐一款自动化测试神器,不会写代码也能做!
321 0
推荐一款自动化测试神器,不会写代码也能做!
|
自然语言处理 Java 测试技术
告别祈祷式编程|单元测试在项目里的正确落地姿势
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Jav...
170 1