On Github zupzup / jug-spock-talk
import spock.lang.*; class HelloSpock extends Specification { ... }a spock test class starts like this
import spock.lang.*; class HelloSpock extends Specification { def "adds two numbers" () { ... } }test cases are named like this
import 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 syntax
import 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 syntax
import 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 test
import 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 evaluated
import 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 documented
import 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 expected
import 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 9
Condition not satisfied: max(a, b) == c | | | | | 3 1 3 | 2 falsespock'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 obsolete
class 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 falsethis can be seen here, the values of left, right and maximum are shown in the test-outcome
class 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 iteration
class 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 this
class 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 approaches
class 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 null
class 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 way
5 * 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 zero
1 * _.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 argumentsmocks can also be validated using wildcards for example, this shows that SOME mock's get method was called go through examples
when: 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-blocks
cache.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 wellto create stubs, methods which return fake values, spock provides really nice, concise syntax Of course, mocks and stubbing can be used together
class 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