优质博文:IT-BLOG-CN
一、简介
Spock
是一个基于Groovy
语言的测试和规范框架,使得测试代码更简介,得益于JUnit Runner
,Spock
兼容大部分IDE
和测试框架JUnit/JMock/Powermock
等。基于BDD行为驱动开发,功能非常强大。提供了各种标签,并采用简单、通用、结构话描述语言,让编写测试代码更加简介、高效。官方文档
为什么要使用
Spock
:因为它优美而富有表现力的规范语言。Spock
的灵感来自JUnit
、RSpec
、jMock
、Mockito
、Groovy
、Scala
、Vulcans
。
主流单元测试框架比较
特性/框架 | JUnit | Mockito | PowerMock | JMock | EasyMock | Spock |
---|---|---|---|---|---|---|
语言 | Java | Java | Java | Java | Java | Groovy(兼容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
内置了Mocking
和Stubbing
的功能,无需依赖第三方(如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() == "测试用字串"
}
}