On Github zupzup / jug-spock-talk
import spock.lang.*;
class HelloSpock extends Specification {
...
}
a spock test class starts like thisimport spock.lang.*;
class HelloSpock extends Specification {
def "adds two numbers" () {
...
}
}
test cases are named like thisimport spock.lang.*;
class HelloSpock extends Specification {
def "adds two numbers" () {
expect:
add(a, b) == c
where:
a | b || c
1 | 7 || 8
4 | 4 || 8
7 | 1 || 8
}
}
this is a whole test
with an expect and where block
showcasing data tables which
we'll get to later
this is the expect/where syntaximport spock.lang.*;
class HelloSpock extends Specification {
def "adds to the wallet" () {
setup:
Wallet wallet = new Wallet()
}
}
setup is used to create objects, mocks,
spies, stubs etc.import spock.lang.*;
class HelloSpock extends Specification {
def "adds to the wallet" () {
given:
Wallet wallet = new Wallet()
}
}
given is the same as setup
with behaviour driven syntaximport spock.lang.*;
class HelloSpock extends Specification {
def "adds to the wallet" () {
given:
Wallet wallet = new Wallet()
when:
wallet.add(100)
}
}
the when-block describes the stimulus of the test
the action you really want to testimport spock.lang.*;
class HelloSpock extends Specification {
def "adds to the wallet" () {
given:
Wallet wallet = new Wallet()
when:
wallet.add(100)
then:
wallet.get() == 100
}
}
the then-block is the
response to the stimulus, where
conditions, exceptions and interactions
are evaluatedimport spock.lang.*;
class HelloSpock extends Specification {
def "adds to the wallet" () {
given: "there is a wallet"
Wallet wallet = new Wallet()
when: "100 euros are added"
wallet.add(100)
and: "another 100 euros are added"
wallet.add(100)
then: "there are 200 euros in the wallet"
wallet.get() == 200
}
}
the and-block is just for documentation,
all blocks can be documentedimport spock.lang.*;
class HelloSpock extends Specification {
def "adds to the wallet" () {
given:
Wallet wallet = new Wallet()
when:
wallet.add(100)
then:
wallet.get() == 100
cleanup:
wallet.delete()
}
}
the cleanup method does exactly what is expectedimport spock.lang.*;
class HelloSpock extends Specification {
def "computing the maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a << [5, 3]
b << [1, 9]
c << [5, 9]
}
}
where is always last in a test method and used
for data tables
this test for example, creates two versions of the test
a = 5, then 3
b = 1, then 9
c = 5, then 9Condition not satisfied:
max(a, b) == c
| | | | |
3 1 3 | 2
false
spock's failure reporting is awesome
it shows all values in an assertion as well
as the outcome of the assertion
this can make debugging tests obsoleteclass UnrollingTest extends Specification {
@Unroll
def "maximum of #left and #right
is #maximum" (int left, int right, int maximum) {
expect:
Math.max(left, right) == maximum
where:
left | right | maximum
1 | 3 | 2
}
}
This test shows unrolling, a very cool feature
where the variables can be added to the name
of the test, and their values will be shown
in the error- maximum of 1 and 3 is 2 FAILED
Condition not satisfied:
Math.max(left, right) == maximum
| | | | |
3 1 3 | 2
false
this can be seen here, the values
of left, right and maximum
are shown in the test-outcomeclass StackSpec extends Specification {
def "throws on pop when the stack is empty" () {
given:
stack = new Stack()
when:
stack.pop()
then:
thrown EmptyStackException
}
}
Exceptions, of course, can be handled as well
and as could be expected
there is also notThrown@Shared resource = new ExpensiveResource()Another cool thing are shared resources, which shares the annotated resource between test methods
class DataDriven extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
3 | 5 || 5
7 | 0 || 7
0 | 0 || 0
}
}
data tables are one of the great
features of spock, in the where block
you can specify variable's values
and each of the values is evaluated
in a seperate iterationclass DataDriven extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a << [3, 7, 0]
b << [5, 0, 0]
c << [5, 7, 0]
}
}
it can also be written like thisclass DataDriven extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b
}
}
or in general, just executing code
you can also combine the different approachesclass DataDriven extends Specification {
def "transform numbers" () {
given:
NumTransformer trans = new NumTransformer()
expect:
trans.transform(arr) == res
where:
arr || res
null || null
[] || []
[1] || [2]
[1, 2, 3] || [2, 3, 4]
}
}
a great real-life use-case are functions
taking an array, you usually want the 0-case,
the 1-case and the many-case as well as nullclass InteractionTest extends Specification {
def "" () {
given:
ProductService productService = new ProductService()
Cache cache = Mock()
productService.setCache(cache)
when:
productService.getProduct('123')
then:
1 * cache.get('products', '123')
}
}
Interaction-based testing focuses on an object's
behaviour instead of state, it deals with their
interactions using mocks, stubs and spies
the example shows some productservice
which goes to a cache
the amount is checked in a very nice way5 * cache.get('products', '123')
0 * cache.get('products', '123')
(1..5) * cache.get('products', '123')
(1.._) * cache.get('products', '123')
(_..5) * cache.get('products', '123')
_ * cache.get('products', '123')
there are different ways to check
how often a method was invoked
5 times
zero times
1 to 5 times
1 to x times
x to 5 times
any number of times, including zero1 * _.get()
1 * cache./get.*s/("123") // getProducts, getArticles
1 * cache._('products', '123') // any method on cache
1 * cache.get('products', '123') // normal
1 * cache.get('products', '!123') // not '123'
1 * cache.get('products', _ as String) // any string
1 * cache.get('products', {it.size() > 2}) // any string longer than 2
1 * cache.get('products', _) // any code
1 * cache.get(*_) // any list of arguments
mocks can also be validated using wildcards
for example, this shows that SOME mock's get
method was called
go through exampleswhen:
productService.getArticle('123')
then:
1 * cache.get('products', '123')
then:
1 * cache.get('images', '123')
to check the order of invocations
just use multiple then-blockscache.get(_) >> new Product('123')
cache.get('123') >> new Product('123')
cache.get('789') >> new Product('789')
cache.get(_) >> [new Product('789'), new Product('123')] // multiple
// compute return value
cache.get(_) >> { args -> args[0] == '123' ? new Product('123') : null}
cache.get(_) >> { throw new NoCacheActiveException() }
// closures can be chained as well
to create stubs, methods which return fake
values, spock provides really nice, concise syntax
Of course, mocks and stubbing can be used togetherclass InteractionTest extends Specification {
def "calls real methods" () {
given:
cache = Spy(Cache)
when:
productService.getProduct('123') // calls the real cache inside
then:
1 * cache.get('products', '123')
}
}
a spy is based on a real method on a real object,
it basically just calls the real method, but records
interactions
spies can also be stubbed, and in a closure
they can call the real method and be extended,
this is how partial mocks can be created