参考资料:https://juejin.cn/post/6844903957475622926#heading-21
背景:
Spock是Java和Groovy应用程序的测试和规范框架
测试代码使用基于groovy语言扩展而成的规范说明语言(specification language)
通过junit runner调用测试,兼容绝大部分junit的运行场景(ide,构建工具,持续集成等)
扩展:
groovy:是以扩展java为目的为设计的JVM语言,可以使用java语法和api,广泛应用于:jenkins,elasticsearch,gradle
BDD:Behavior-driven development行为驱动测试
通过某种规范说明语言去描述程序“应该”做什么,再通过一个测试框架读取这些描述、并验证应用程序是否符合预期。把需求转化成Given/When/Then的三段式
依赖
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.1-groovy-2.4</version>
<scope>test</scope>
</dependency>
Demo
public interface CacheService {
String getUserName();
}
public class Calculator {
private CacheService cacheService;
public Calculator(CacheService cacheService) {
this.cacheService = cacheService;
}
public boolean isLoggedInUser(String userName) {
return Objects.equals(userName, cacheService.getUserName());
}
...
}
class CalculatorSpec extends Specification {
// mock对象
// CacheService cacheService = Mock()
def cacheService = Mock(CacheService)
def calculator
void setup() {
calculator = new Calculator(cacheService)
}
def "is username equal to logged in username"() {
// stub 打桩
cacheService.getUserName(*_) >> "Richard"
when:
def result = calculator.isLoggedInUser("Richard")
then:
result
}
...
}
Spock深入
在Spock中,待测系统(system under test; SUT) 的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类需要继承自Specification类。命名遵循Java规范。
结构:
每个测试方法可以直接用文本作为方法名,方法内部由given-when-then
的三段式块(block)组成。除此以外,还有and
、where
、expect
等几种不同的块。
@Title("测试的标题")
@Narrative("""关于测试的大段文本描述""")
@Subject(Adder) //标明被测试的类是Adder
@Stepwise //当测试方法间存在依赖关系时,标明测试方法将严格按照其在源代码中声明的顺序执行
class TestCaseClass extends Specification {
@Shared //在测试方法之间共享的数据
SomeClass sharedObj
def setupSpec() {
//TODO: 设置每个测试类的环境
}
def setup() {
//TODO: 设置每个测试方法的环境,每个测试方法执行一次
}
@Ignore("忽略这个测试方法")
@Issue(["问题#23","问题#34"])
def "测试方法1" () {
given: "给定一个前置条件"
//TODO: code here
and: "其他前置条件"
expect: "随处可用的断言"
//TODO: code here
when: "当发生一个特定的事件"
//TODO: code here
and: "其他的触发条件"
then: "产生的后置结果"
//TODO: code here
and: "同时产生的其他结果"
where: "不是必需的测试数据"
input1 | input2 || output
... | ... || ...
}
@IgnoreRest //只测试这个方法,而忽略所有其他方法
@Timeout(value = 50, unit = TimeUnit.MILLISECONDS) // 设置测试方法的超时时间,默认单位为秒
def "测试方法2"() {
//TODO: code here
}
def cleanup() {
//TODO: 清理每个测试方法的环境,每个测试方法执行一次
}
def cleanupSepc() {
//TODO: 清理每个测试类的环境
}
setup与given
def "is username equal to logged in username"() {
setup://习惯于携程given:
def str = "Richard"
// stub 打桩
cacheService.getUserName(*_) >> str
when:
def result = calculator.isLoggedInUser("Richard")
then:
result
}
when_then与wxpect
when:
def x = Math.max(1, 2)
then:
x == 2
//等价于
expect:
Math.max(1, 2) == 2
assert
条件类似junit中的assert,就像上面的例子,在then或expect中会默认assert所有返回值是boolean型的顶级语句**。**如果要在其它地方增加断言,需要显式增加assert关键字
异常断言:
def "peek"() {
when: stack.peek()
then: thrown(EmptyStackException)
}
//如果要验证没有抛出某种异常,可以用notThrown()
Mock
创建对象
def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
Subscriber subscriber = Mock()
Subscriber subscriber2 = 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
}
调用频率约束
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')
目标约束
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object
方法约束
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')
参数约束
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)
Spy
和Mock的区别是,Spy使用类的原有方法,有打桩则使用打桩方法;Mock使用类的打桩方法,如果没有打桩,则类的所有方法默认都返回null。
//spy的标准是:如果不打桩,默认执行真实的方法,如果打桩则返回桩实现。
List<String> list = new LinkedList<String>();
List<String> spy = spy(list);
when(spy.size()).thenReturn(100);
spy.add("one");
spy.add("two");
assertEquals(spy.get(0), "one");
assertEquals(100, spy.size());
Stub打桩
subscriber.receive(_) >> "ok"
| | | |
| | | response generator
| | argument constraint
| method constraint
target constraint
如:subscriber.receive(_) >> "ok"
意味,不管什么实例,什么参数,调用 receive 方法皆返回字符串 ok
返回固定值
subscriber.receive(_) >> "ok"
//subscriber.receive(_) >> mockOk()
//private String mockOk(){
// return "ok"
//}
返回值序列
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
动态计算返回值
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
产生副作用
subscriber.receive(_) >> { throw new InternalError("ouch") }
链式响应
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"