让单测变得如此简单 -- spock 框架初体验

2022-06-27 15:52:11 浏览数 (1)

1. 引言

测试流程在软件开发过程中显得越来越重要了,因为无论经验多么丰富的开发者,都难免在编码过程中出现失误甚至是逻辑错误,在这样的前提下,单元测试就显得非常重要了。 单元测试通过对程序中每个部分进行独立的测试覆盖,且在每次代码更新后自动执行,保证了新的修改不会影响到旧的功能。 可以说,编写单元测试让程序员尽早的发现问题、暴露问题,从而让整个编码过程更为可控,同时,编写单元测试过程中对细节的关注,也让程序员更多的思考自己编写的程序的健壮性。 但单元测试又意味着我们需要在维护业务代码的同时,额外维护单元测试的流程和用例,无疑增加了维护成本,而对于程序开发的交接工作来说,除了文档、业务代码,还需要阅读和理解前人的单元测试流程,无疑也让新人的上手难度大为增加。 既然单元测试如此重要,那么我们是否可以找到一个编写高效、易于维护、简单易懂的单元测试框架呢?java 中的 spock 正是凭借这样的理念而诞生的一种测试框架。

2. spock

题外话,提起 spock,大概你最先想到的是《星际迷航》吧

此前,我们介绍过 java 的另一个测试框架 — JUnit

JUnit 是一套使用通过 java 语言实现的一套成熟的单元测试工具,但因为 java 本身的复杂性,JUnit 通常需要维护大量的代码来实现非常基础的测试功能,如果你还需要 mock 等额外的测试功能,你还需要引入 mokito 等其他框架,无疑增加了学习成本。 spock 是通过 groovy 实现的,groovy 是一种在 jvm 下运行的动态语言,与 java 最主要的区别就在于 groovy 拥有更强的语义,编写灵活,可读性强,虽然对于编写较大的项目来说,动态语言往往因为其过度的灵活性造成项目成员编码风格多样而难以维护,但对于单元测试这样追求便捷与高效的场景来说,动态语言相较于 java 则更加游刃有余,而同时,groovy 兼容 java 语法,可以直接调用 java api,这让 java 程序员上手毫无难度。 下面就让我们来实战一下。

3. 准备工作

3.1. 引入依赖

使用 spock 框架,我们首先需要引入下面的 maven 依赖,来拉取所需的一系列 jar 包。

代码语言:javascript复制
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-test</artifactId>  
    <scope>test</scope>  
</dependency>  
<!-- https://mvnrepository.com/artifact/org.spockframework/spock-core -->  
<dependency>  
    <groupId>org.spockframework</groupId>  
    <artifactId>spock-core</artifactId>  
    <version>1.3-groovy-2.5</version>  
    <scope>test</scope>  
</dependency>  
<!-- https://mvnrepository.com/artifact/org.spockframework/spock-spring -->  
<dependency>  
    <groupId>org.spockframework</groupId>  
    <artifactId>spock-spring</artifactId>  
    <version>1.3-groovy-2.5</version>  
    <scope>test</scope>  
</dependency>  
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->  
<dependency>  
    <groupId>org.codehaus.groovy</groupId>  
    <artifactId>groovy-all</artifactId>  
    <version>3.0.5</version>  
    <type>test</type>  
</dependency>

3.2. 创建被测试类

下面我们编写一个极为简单的作为示例的计算器类吧。

代码语言:javascript复制
package cn.techlog.testspring.testspring.service;  

public class Calculate {  
    public int add(int num1, int num2) {  
        return num1   num2;  
    }  

    public int sub(int num1, int num2) {  
        return num1 - num2;  
    }  

    public int mul(int num1, int num2) {  
        return num1 * num2;  
    }  

    public int div(int num1, int num2) {  
        return num1 / num2;  
    }  
}

4. 单元测试的编写

4.1. 创建测试类

在 test 路径下,我们创建一个名为 CalculateTest 的 groovy class:

代码语言:javascript复制
package service  

import spock.lang.Specification  

class CalculateTest extends Specification {  
    Calculate calculate = new Calculate()  
}

在这个类中,我们创建了一个成员变量 calculate,用于接下来测试使用。

4.2. expect — 最基本的验证

代码语言:javascript复制
    def "test add"() {  
        expect:  
            calculate.add(1, 2) == 3  
    }

def 是 groovy 的关键字,他可以用来创建变量或方法,这里我们创建了一个名为“test add”的测试方法。 expect 关键字让我们能够实现最简单的验证,如果传递参数 1 和 2,返回值不为 3,则这条测试用例就会执行失败。

执行测试方法,我们就可以看到测试的结果:

如果执行失败,则会展示:

4.3. expect where — 实现多条测试用例的测试

上面的示例我们只使用了一个测试用例,但往往我们希望能够实现多个测试用例的批量测试。 这个时候,where 关键字就派上用场了。

代码语言:javascript复制
    def "test add"() {  
        expect:  
            calculate.add(num1, num2) == result  
        where:  
            num1 | num2 | result  
            1    | 1    | 2  
            3    | 0    | 3  
            4    | 2    | 6  
    }

我们也可以换一种写法:

代码语言:javascript复制
    def "test add"() {  
        expect:  
        calculate.add(num1, num2) == result  
        where:  
        num1 << [1, 3, 4]  
        num2 << [1, 0 ,2]  
        result << [2, 3, 6]  
    }

这样,当我们运行的时候,就会顺次执行每一个用例,让测试更加方便。

4.4. @Unroll 注解 — 让测试结果分条展示

上图中,虽然我们运行了多个测试用例,但结果却显示在一条结果中,这样,当我们的用例中某条出错时,是难以直观的定位到的,既然是多个用例,我们预期中当然是每个用例单独占用一行结果来显示。 spock 框架也提供了批量测试拆分的机制,只要在方法上加上 @Unroll 注解,多个测试用例就会在结果中被分开展示了。

4.5. when then — 进阶的测试场景

有些测试场景使用 expect 很难实现,例如我们预期函数抛出异常的操作,此时可以通过 when then 块来实现。

代码语言:javascript复制
    def "test div"() {  
        when:  
        calculate.div(1, 0)  

        then:  
        def ex = thrown(ArithmeticException)  
        ex.message == "/ by zero"  
    }

4.6. @Timeout — 测试超时

在方法上添加 @Timeout 注解,可以实现测试用例超时的指定。

代码语言:javascript复制
    @Timeout(value = 900, unit = TimeUnit.MILLISECONDS)  
    def "test div"() {  
        when:  
        calculate.div(1, 0)  

        then:  
        def ex = thrown(ArithmeticException)  
        ex.message == "/ by zero"  
    }

4.7. with — 测试对象成员

上面都是测试的基本类型的数字,如果我们要测试一个对象的每个字段的值是否符合预期呢?通过上述方法都会显得比较繁琐。 with 就是用来解决这个问题的。

代码语言:javascript复制
    def "test findByName by verity"() {  
        given:  
        def userDao = Mock(UserDao)  

        when:  
        userDao.findByName("kk") >> new User("kk", 12, "33")  

        then:  
        def user = userDao.findByName("kk")  
        with(user) {  
            name == "kk"  
            age == 12  
            passwd == "33"  
        }  
    }

5. Mock 测试

在工程项目中,我们编写的程序往往依赖于外部的接口调用,但在单测环节,我们应该做到保证我们的程序在外部接口返回正确的前提下结果的正确性,但由于实际的运行环境、权限等等条件的限制,我们往往不能在例行的自动化单元测试中真的去调用外部接口,此时就体现出 Mock 测试的重要性。 Mock 测试通过模拟外部调用的结果,让我们的测试程序得以继续运行,在 JUnit 中,我们需要使用 Mockit 来实现接口的 Mock,同时,Mock 的编写也较为复杂,这些在 spock 中就显得非常简单了。

5.1. 准备工作

让我们将 Calculate 类稍作改变,作为 sub 方法第二个参数的减数从另一个服务中获取,这个服务需要一个参数就是我们的被减数:

代码语言:javascript复制
package cn.techlog.testspring.testspring.service;  

import org.springframework.stereotype.Service;  

import javax.annotation.Resource;  

@Service  
public class Calculate {  
    @Resource  
    private RemoteGateway remoteGateway;  

    public int add(int num1, int num2) {  
        return num1   num2;  
    }  

    public int sub(int num1) {  
        return num1 - remoteGateway.getSubtraction(num1);  
    }  

    public int mul(int num1, int num2) {  
        return num1 * num2;  
    }  

    public int div(int num1, int num2) {  
        return num1 / num2;  
    }  
}

5.2. Mock 测试类

我们的测试类要做一些相应的改动,来创建我们的被测试类实例与被 Mock 类实例:

代码语言:javascript复制
package service  

import cn.techlog.testspring.testspring.service.Calculate  
import cn.techlog.testspring.testspring.service.RemoteGateway  
import spock.lang.Specification  
import spock.lang.Unroll  

class CalculateTest extends Specification {  
    RemoteGateway remoteGateway = Mock(RemoteGateway)  
    Calculate calculate = new Calculate(remoteGateway: remoteGateway)  
}

5.3. 编写测试方法

代码语言:javascript复制
    def "test sub"() {  
        when:  
        remoteGateway.getSubtraction(*_) >> subtraction  

        then:  
        calculate.sub(num1) >> result  

        where:  
        num1 << [1, 3, 4]  
        subtraction << [1, 2, 3]  
        result << [0, 1, 1]  
    }

6. 公共方法

JUnit 有一个很方便的功能,那就是可以定义每个测试方法开始前与结束后调用的方法,以便做一些公共的自动处理功能,spock 也提供了相应的机制:

方法

说明

setup()

每个方法执行前调用

cleanup()

每个方法执行后调用

setupSpec()

每个方法类加载前调用一次

cleanupSpec()

每个方法类执行完调用一次

7. 参考资料

http://spockframework.org/spock/docs/1.3/all_in_one.html。 https://blog.csdn.net/u011719271/article/details/102888009。

0 人点赞