Spock框架初体验:更优雅地写好你的单元测试

何为单元测试

在介绍本期的主角Spock之前,让我们先来了解一下什么是单元测试:

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于【单元】的含义,一般来说,要根据实际情况判定具体含义,如C语言中单元指一个函数,Java里单元指一个类等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。 ---------百度百科

总结来说,**单元测试就是对代码中的一个最小的单元模块进行测试的手段。**了解了单元测试是什么,那么我们为什么需要进行单元测试呢?单元测试的必要性是什么呢?下面有一段很经典的话,也是启发我开始决定编写单元测试的原因。

“代码不写测试就像上了厕所不洗手……单元测试是对软件未来的一项必不可少的投资。” -------XML之父Tim Bray

单元测试,从测试层面来说,其属于测试的最基础的一环,也是发现bug时候修改所耗费时间最少的环节。下面这张图,是来自微软的统计数据,其大体想说的内容就是,bug在单元测试阶段被发现,平均需要耗时3.25小时;但是如果漏到系统测试阶段,则要花费11.5小时进行修复。

除此以外,一些学者们经过统计还绘制出了下图。从下图可以发现,85%的缺陷都在代码设计阶段产生,而发现bug的阶段越靠后,修复相应bug所耗费成本就越高。由此看来,单测代码的编写对于软件交付质量以及人工耗费成本都有及其重要的影响。

同时,从另外一方面角度来说,单测的代码是保证你在提测前代码质量的主要手段之一,如果你的单测代码设计足够合理,交付给测试的代码质量足够高,日积月累你在旁人眼中的形象也就相对靠谱,有助于你未来事业的进步和发展。

Spock框架简介

在介绍Spock框架之前,让我们先来回顾下传统JAVA代码,在Junit框架下是如何编写测试用例的。这里我举《算术珠玑》中一个计算税金的例子来介绍:

    public BigDecimal calcTax(double income) {
        BigDecimal tax;
        BigDecimal salary = BigDecimal.valueOf(income);
        if (income <= 0) {
            return BigDecimal.ZERO;
        }
        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);
    }

可以看到依据收入的不同,代码需要分别计算不同情况下的税收。在采用Junit的时候,我们的代码大概率会如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = DemoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("local")
@Slf4j
public class UnitTestJunit {

    @Autowired
    UnitTestService unitTestService;

    //测试税款为0的情况
    @Test
    public void testCalcTax(){
        double income = 0;
        BigDecimal res = unitTestService.calcTax(income);
        assert res .compareTo(BigDecimal.valueOf(0)) == 0;
    }

    // 测试税款为2500
    @Test
    public void testCalcTax1(){
        double income = 2500;
        BigDecimal res = unitTestService.calcTax(income);
        assert res.compareTo(BigDecimal.valueOf(75)) == 0;
    }

    //其余测试情况
    ...
}

可以看到,随着情况的增多、if-else语句的增加,Junit带来的代码量会逐渐的增长,为后续的维护和情况的增加带来了一定的成本。针对这个痛点,Spock提出了自己的解决方案。其通过采用where语句的方式,简化了多if-else语句情况下的条件判断,进而简化了需编写的代码数量

下面我们就逐步来介绍,如何采用Spock优化单元测试代码。首先,由于Spock框架底层是基于Groovy脚本语言运行的,因此我们需要在maven依赖中导入如下的三个pom依赖:

//maven pom依赖 需要注意的是这三个pom间有相应的依赖关系,错误的版本可能导致脚本无法运行
    <dependency>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-all</artifactId>
        <version>2.4.9</version>
    </dependency>
    <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-spring</artifactId>
        <version>1.2-groovy-2.4</version>
    </dependency>
    <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-core</artifactId>
        <version>1.2-groovy-2.4</version>
    </dependency>

同时,需要注意导入Groovy语言相应的插件,以确保Spock框架能够正常的运行。

// maven插件依赖,要启动脚本需要该插件
    <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>

完成了pom文件的加载后,咱们就可以开始编写相应的代码了。依旧以税金为例子,如果需要使用Spock框架进行测试的话,则编写的代码大致如下:

class UnitTestServiceTest extends Specification {

    @Shared
    UnitTestService unitTestService;

    def setupSpec() {
        unitTestService = new UnitTestService()
    }

    @Unroll
    def "税金计算 收入#salary 税收#tax"() {
        given: "准备数据"

        when: "测试方法"
        BigDecimal taxRes = unitTestService.calcTax(salary);

        then: "校验结果"
        assert tax == taxRes

        // 关键性代码,
        where: "表格方式进行测试"
        salary || tax
        // 对税收在0-3000阶段进行测试的代码
        20     || 0.6
        30     || 0.9
        3001   || 90.1
        12000  || 990
    }
}

可以看到,代码明显缩短了很多,而且未来如果需要拓展单测代码的话,也只需要紧接着where继续往下写即可。同时配合上Spock框架自带的@Unroll注解的方式,可以更直观的观察到对应情况下代码是否可以正常运行,有助于后人对代码的维护。如下是代码运行起来的结果:

在这里插入图片描述

(ps:在Macos系统下,采用option+Command + L可以快速整理脚本格式,在写where语句时有奇效)

回顾上述代码,我们注意到在Spock框架中新建变量的两个关键点 @Shared注解setupSpec()方法,这两个关键点一定程度上体现了Spock框架的设计思路。

我们首先介绍@Shared注解的作用,其同java中的关键字static作用一样,都是为了在当前类初始化之前,提前将对应的对象加载进来,并用于初始化过程中。

    @Shared
    UnitTestService unitTestService; //静态变量对象,其类加载的顺序相对靠前

	static UnitTestService unitTestService; //同上述代码效果一致 

至于setupSpec方法,则是Spock测试框架中的一个固定方法,完整的四个方法如下所示:(另外,下述四个方法同Junit中的相关注解其实一一对应)

SpockJunit作用
def setup()@Before每个测试方法开始前都会执行一遍
def cleanup()@After每个测试方法后都会执行一遍
def setupSpec()@BeforeClass在第一个测试方法开始前执行一遍
def cleanupSpec()@AfterClass最后一个测试方法后执行

同时注意到,为了保持良好的阅读性,Spock框架推荐在编写测试用例的时候使用特定的模版,即:

given/setup: "初始化数据||mock相应数据"

when: "执行方法获取结果"
    
then: "方法后再做处理"
    
expect: "校验相应的数据"
    
cleanup: "数据清理"
        
where: "多种情况下的条件校验"

为什么要这么设计呢?原因是Spock认为测试方法是测试类的核心,而一个测试方法大体都应由4部分组成:

  1. Setup:环境初始化(可选)
  2. Stimulus:调用待测试代码
  3. Response:描述预期行为
  4. Cleanup:清理资源(可选)

而上述模版正是Spock框架对于测试方法阶段的一种具象化表现。

在这里插入图片描述

通过采用这个模版,可以更简洁明了的介绍我们的单测代码是如何运行的,在后续需要增加业务功能,亦或者别人开发的时候,也都能提供很好的帮助。

总结与思考

​ 对于单元测试,我在了解的过程中也产生了一些思考。主要的几个思考点如下:

1、什么样的代码块需要进行单元测试?或者说所有代码都需要写单元测试吗?

​ 最好的情况当然是对所有代码都进行单元测试。但是,这样做的话工作量将会十分巨大,较紧的工期里往往难以完成全代码量的覆盖。另一方面来说,全代码覆盖单测,往往会产生边际效应,即投入远远大于产出。因此,个人认为只针对业务代码中的关键模块进行单元测试就足以满足质量需求。最关键的点在于,要清楚的意识到编写单测代码本身不是目的,实现代码质量提高才是最终的目的~

2、怎么证明自己的单元测试是有效果的?

​ 总体来说,目前没有一个直观的指标能说明单元测试对代码质量有明显的提升和增长。客观上来说,只能通过一些间接指标来体现单测代码对于代码质量的影响。在这里主要列举了如下五个指标:

  1. bug类指标(间接指标):连续迭代的bug总数趋势、迭代内新建bug的趋势、千行bug率
  2. 单测的需求覆盖度(50%以上),参与人员覆盖度(80%以上)
  3. 单测case总数趋势,代码行增量趋势
  4. 增量代码的行覆盖率(接入层80%,客户端30%)
  5. 单函数圈复杂度(低于40),单函数代码行数(低于80),扫描告警数。

PS:对于Spock框架的学习,本人还停留在一个相对基础的层面,后续会在使用这个框架的过程中不断去迭代优化这篇文章的内容,也十分期待各位读者能给我提出有建设性的意见~

如果你看到了这里,不妨给我点个赞、点个收藏,要是还能关注一下下就更好啦~

创作不易,感谢支持~

参考文章

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

单元测试到底是什么?应该怎么做?

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值