Spock Unit Test in Java

优质博文:IT-BLOG-CN
在这里插入图片描述

一、简介

Spock是一个基于Groovy语言的测试和规范框架,使得测试代码更简介,得益于JUnit RunnerSpock兼容大部分IDE和测试框架JUnit/JMock/Powermock等。基于BDD行为驱动开发,功能非常强大。提供了各种标签,并采用简单、通用、结构话描述语言,让编写测试代码更加简介、高效。官方文档

为什么要使用Spock:因为它优美而富有表现力的规范语言。Spock的灵感来自JUnitRSpecjMockMockitoGroovyScalaVulcans

主流单元测试框架比较

特性/框架JUnitMockitoPowerMockJMockEasyMockSpock
语言JavaJavaJavaJavaJavaGroovy(兼容Java)
主要用途单元测试、集成测试Mock对象的创建和验证Mock静态方法、构造函数和私有方法Mock对象的创建和验证Mock对象的创建和验证行为驱动开发(BDD)、单元测试、集成测试
BDD支持部分支持(通过扩展)不支持不支持不支持不支持原生支持
Mocking需要与Mockito或其他库结合原生支持Mock扩展Mockito,支持Mock静态方法和私有方法原生支持Mock原生支持Mock原生支持Mock|
数据驱动测试支持(需要额外的库,如JUnitParams不支持不支持不支持不支持原生支持
语言简洁性较为简洁,但需要一定的配置非常简介复杂(需要结合Mockito使用,配置较多)较为复杂(需要配置和学习JMock特有API)较为简洁,API清晰非常简介(DSL语法)
错误报告标准错误报告标准错误报告标准错误报告标准错误报告标准错误报告详细的错误和断言失败信息
IDE支持优秀(广泛支持)优秀(广泛支持)较好(但需要与Mockito结合)较好(但使用者较少)较好(广泛支持)良好(主流IDE支持,但部分功能可能不如JUnit完善
社区和生态系统非常成熟和广泛成熟和广泛较小(依赖Mockito的社区和生态系统)相对较少相对成熟相对较少,但在增长中
学习曲线低(大多数Java开发者熟悉)低(与JUnit结合使用时)高(需要学习Mockito和PowerMock的结合使用)中等(需要学习JMock API和配置)低(简单易学)中等(需要学习Groovy语法,但DSL使测试更直观
性能高性能高性能较高性能开销(复杂的Mock和字节码操作)高性能高性能较好的性能(但Groovy可能带来一些开销

二、优点

【1】简介且可读性强的语法: Spock使用Groovy语言编写测试脚本,其DSL领域特定语言使得测试代码非常简洁和可读。描述性的方法名:setup()/when()/then()使得测试逻辑一目了然。

内置多种标签来规范单元测试代码,使测试代码更规范、结构更清晰、可读性高、降低后续维护成本。

Spock是基于Groovy的测试框架,Groovy是一种在Java平台上的强大动态语言,简洁、灵活。相比PowerMock/JMock等框架需要在Java中编写,语法相对更加繁琐。Groovy的动态语言特性使得编写测试代码更加简洁和易读。

【2】数据驱动测试: Spock非常强大地支持数据驱动测试,通过where块可以方便地定义多组输入输出,减少重复代码,增加单元测试覆盖率。

【3】强加的错误抱错: Spock提供了详细的错误报告和断言失败信息,使得调试和修复问题更加容易。错误信息通常包含详细的上下文,使得定位问题更加直接。

【4】内置Mocking支持: Spock内置了MockingStubbing的功能,无需依赖第三方(如Mockito)。

【5】BDD风格支持: Spock天然支持行为驱动开发(BDD)风格的测试、这使得编写和维护测试变得更加直观。BDD风格的测试不仅描述了测试的行为,还能更好地表达业务逻辑,有助于提升代码质量。

三、 缺点

【1】IDE支持: 主流的IDE都支持Spock,但对于断点调试和代码补全等支持没有JUnit那样成熟。

【2】性能开销: 由于Spock使用Groovy作为脚本语言,可能会带来一定的性能开销。尽管开销大多数可以忽略,但是对性能极为敏感的场景下需要注意。

【3】社区生态较小: 虽然Spock功能强大,但相比JUnit/Spock的社区和生态系统相对较小。这意味着在遇到问题时,可用的资源和支持较少。

【4】学习曲线: 对于没有Groovy背景的开发者而言,学习和掌握Spock的语法需要一些时间。

四、环境配置

【1】pom依赖配置

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-bom</artifactId>
      <version>2.3-groovy-4.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement><dependencies>
  <dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-junit4</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies><plugins>
    <!-- Mandatory plugins for using Spock -->
    <plugin>
      <!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin,
      visit https://github.com/groovy/GMavenPlus/wiki -->
      <groupId>org.codehaus.gmavenplus</groupId>
      <artifactId>gmavenplus-plugin</artifactId>
      <version>3.0.2</version>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>compileTests</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <!-- Optional plugins for using Spock -->
    <!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) -->
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.2.5</version>
      <configuration>
        <useFile>false</useFile>
        <includes>
          <include>**/*Test</include>
          <include>**/*Spec</include>
        </includes>
      </configuration>
    </plugin>
</plugins>

【2】Spock测试结构:测试类与规范。目录结构如下:

--src
    --main
    --test
        --groovy
            --com.flight.xxx(包名)
                --XXXSpec.groovy
                --XXXTest.groovy

五、测试方法的生命周期

junit使用时,主要用以下注解来标记测试类的方法:

@Test :标记需要运行的测试方法,一个测试类中可以有多个@Test方法;
@Before/@After :标记的方法,会在每个测试方法运行之前/之后运行一次;
@BeforeClass/@AfterClass :标记的方法会在测试类初始化时/销毁时运行;

spock没有使用以上的注解形式,而是测试类需要继承 Specification 父类,重写父类中的以下方法,就可以自定义测试方法的生命周期:

def setup() {}         // run before every feature method
def cleanup() {}       // run after every feature method
def setupSpec() {}     // run before the first feature method
def cleanupSpec() {}   // run after the last feature method

测试代码

package com.yawn.spock
 
import spock.lang.Shared
import spock.lang.Specification
 
/**
 * spock 测试
 */
class CalculateSpec extends Specification {
 
    // 初始化
    def setupSpec() {
        calculateService = new CalculateService()
        println ">>>>>>   setupSpec"
    }
    def setup() {
        println ">>>>>>   setup"
    }
    def cleanup() {
        println ">>>>>>   cleanup"
    }
    def cleanupSpec() {
        println ">>>>>>   cleanupSpec"
    }
 
    def "test life cycle"() {
        given:
        def a = 1
        def b = 2
 
        expect:
        a < b
 
        println "test method finished!"
    }
}

六、普通方法

【1】创建mock对象

def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
Subscriber subscriber3 = Mock {
  1 * receive("hello")
  1 * receive("goodbye")
}
 
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()

【2】注入mock对象

class PublisherSpec extends Specification {
    Publisher publisher = new Publisher()
    Subscriber subscriber = Mock()
    Subscriber subscriber2 = Mock()
 
    def setup() {
        publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
        publisher.subscribers << subscriber2
    }
}

【3】调用频率

1 * subscriber.receive("hello") // exactly one call
0 * subscriber.receive("hello") // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1..) * subscriber.receive("hello") // at least one call
(..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello") // any number of calls, including zero
// (rarely needed; see 'Strict Mocking')

【4】目标约束

1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object

【5】方法约束

1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression (here: method name starts with 'r' and ends in 'e')

【6】参数约束

1 * subscriber.receive("hello") // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello") // an argument that is unequal to the String "hello"
1 * subscriber.receive() // the empty argument list (would never match in our example)
1 * subscriber.receive() // any single argument (including null)
1 * subscriber.receive(*) // any argument list (including the empty argument list)
1 * subscriber.receive(!null) // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive(endsWith("lo")) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 && it.contains('a') })
// an argument that satisfies the given predicate, meaning that
// code argument constraints need to return true of false
// depending on whether they match or not
// (here: message length is greater than 3 and contains the character a)

【7】返回固定值

subscriber.receive(_) >> "ok"

【8】返回值序列:返回一个序列,迭代且依次返回指定值。如下所示,第一次调用返回ok,第二次调用返回error,以此类推

subscriber.receive(_) >>> ["ok", "error", "error", "ok"]

【9】动态计算返回值

subscriber.receive() >> { args -> args[0].size() > 3 ? "ok" : "fail" }
subscriber.receive() >> { String message -> message.size() > 3 ? "ok" : "fail" }

【10】产生异常

subscriber.receive(_) >> { throw new InternalError("ouch") }

【11】链式响应

subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"

【12】集成其他测试框架:Powermock集成

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([WorkContextFactory.class])
class NotificationDataNewCollectorTest extends Specification {
}

Mockito集成

@ExtendWith(MockitoExtension)
class MockStaticMethodSpec extends Specification {
    def "mock static method"() {
        given:
        def mock = Mockito.mockStatic(StringUtils)
 
        and:
        mock.when { StringUtils.equalsIgnoreCase(Mockito.any(), Mockito.any()) }.thenReturn(false)
 
        expect:
        result == StringUtils.equalsIgnoreCase(s1, s2)
 
        cleanup:
        mock.close()
 
        where:
        s1  | s2  || result
        "a" | "a" || false
        "a" | "b" || false
    }
}

七、静态方法

静态方法案例

public class TestClass {

    public static String staticMethod() {
        return null;
    }

}

测试类:对于静态方法,私有方法,final方法,在用powermock做单元测试的时候,需要增加注解@PrepareForTest

这个注解的作用就是:该注释告诉PowerMock(ito)列出的类将需要在字节码级别上进行操作。

import org.junit.Rule
import org.mockito.Mockito
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.rule.PowerMockRule
import spock.lang.Specification

@PrepareForTest([TestClass.class])
class MockStaticMethodSpec extends Specification {

    @Rule
    PowerMockRule mPowerMockRule = new PowerMockRule();

    def "测试静态方法"() {
        setup :
        PowerMockito.mockStatic(TestClass.class)

        when :
        Mockito.when(TestClass.staticMethod()).thenReturn("测试用字串")

        then :
        TestClass.staticMethod() == "测试用字串"
    }

}
  • 54
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 45
    评论
评论 45
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿进阶

千言万语都不及一句“谢谢”

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值