Skip to content

Instantly share code, notes, and snippets.

@jiffle
Last active May 9, 2024 14:19
Show Gist options
  • Save jiffle/65e940093cdede0cd38c8960edf86f0d to your computer and use it in GitHub Desktop.
Save jiffle/65e940093cdede0cd38c8960edf86f0d to your computer and use it in GitHub Desktop.
Spock Useful Patterns Cheatsheet

Spock Useful Patterns Cheatsheet

Adding sequences of behaviour to Mocks and Stubs

The >>> operator allows a sequence of values to be returned:

myMock.someCall() >>> ['first value', 'second value', 'third value', 'etc']

This returns each string in turn. Behaviour (such as throwing exceptions) in closures cannot be used by this operator.

The >> operator allows value or behaviour (closures) to be returned

myMock.someCall() >> 'first value' >> { generatorMethod() } >> 'third value' >> { throw new EmptyStackException() }

Each operator returns a single value or closure, but they can be chained (and intermixed with the >>> operator)

Asserting / Capturing Arguments to Mocked Methods

There are a number of different approaches that can be used. Firstly a reminder of the basic assertions that can be applied to parameters:

Basic Parameter / Argument Predicates

Note that these are predicates, not assertions (i.e. they fail by not matching rather than asserting the argument value).

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({ it.size() > 3 }) // an argument that satisfies the given predicate
                                          // (here: message length is greater than 3)

More complex predicate examples

Combining multiple predicate conditions:

1 * worker.doSomething({it.content == 'foo' && it.id > 10})

Or a version with a named parameter for multiple predicates:

1 * eventRecorder.recordEvent({
  event -> 
  event.getTimestamp() != null &&
  event.getType() == Event.Type.REMINDER_SENT &&
  event.getCustomerName() == "Susan Ivanova"
  })

Note the && combining the predicate conditions and the lack of an assert as these are predicates, not assertions!

Asserting the Parameter / Argument Values

As closure(s) on the right hand side

Create explicit assertions on the right hand side - note that using this format allows different assertions to be applied on each method invocation. Remember that if the method needs a return value it needs to be provided at the end of the closure.

Where there is a single argument:

1 * collaborator.someMethod(_) >> { int aParam -> 
  assert aParam == 10 
  // any other assertions...
}

or the single argument from of the array capture (below)

1 * basketsRepository.save(_) >> { arguments ->
  final Apple apple = arguments.get(0)
  assert apple == new Apple(weight: 0.123)    
}

For several arguments capture all the arguments as an array:

1 * greeting.sayHello(*_) >> { arguments ->
  final List<Person> argumentPeople = arguments[0]
  assert argumentPeople == [new Person(name: 'george'), new Person(name: 'ringo')]
}

Capturing the Argument for Later Use

Arguments can be captured for later use in assertions (careful: this may not lead to readable code)

  given:
    Apple apple
    1 * basketsRepository.save(_) >> { arguments ->
        apple = arguments[0]
      }
  when:
    // ...
  then:
    assert apple == new Apple(weight: 0.123)    

Checking Mock Invocation Order

Specify the order in which interactions must take place by adding then sections:

    then:
        1 * service.save(_ as User)
    then:
        1 * transaction.commit()

Advanced Comparisons

Using Groovy Spread Operator

Allows a list of objects to easily be tested for a property of each object:

  then:
    myBooks*.name == ['Guns, Germs & Steel','Jitterbug Perfume']
    myBooks*.publisher*.name == ['Penguin','Walker']

###Using Hamcrest

Compare Lists Ignoring Order (Hamcrest Integration)

build.gradle:

dependencies {
    testCompile 'org.hamcrest:hamcrest-library:1.3'
}

test.spock:

import org.hamcrest.Matchers.*
import static org.hamcrest.Matchers.containsInAnyOrder
import static spock.util.matcher.HamcrestSupport.that

def "Some test that should check results in any order"() {
  // ...
  then:
    that( listThatNeedsToBeTested, containsInAnyOrder( firstControl, secondControl, thirdControl, etc))

Asynchronous Test Constructs

BlockingVariable

A statically typed variable whose get() method will block until some other thread has set a value with the set() method, or a timeout expires.

Optional constructor parameters:

  • timeout: Failure timeout. Default 1 second
    def machine = new Machine()
    def result = new BlockingVariable<WorkResult>
    // register async callback
    machine.workDone << { r ->
      result.set(r)
    }
  when:
   machine.start()
  then:
    // blocks until workDone callback has set result, or a timeout expires
    result.get() == WorkResult.OK

AsyncConditions

Alternative to BloackingVariable. Rather than transferring state to an expect- or then-block, it is verified right where it is captured. On the upside, this can result in a more informative stack trace if the evaluation of a condition fails. On the downside, the coordination between threads is more explicit.

Optional constructor parameters:

  • numEvalBlocks: number of evaluate blocks (corresponds to the number of evaluate { } calls that the test contains)
    def machine = new Machine()
    def conds = new AsyncConditions()
    // register async callback
    machine.workDone << { result ->
      conds.evaluate {
        assert result == WorkResult.OK
        // could add more explicit conditions here
        }
      }
  when:
    machine.start()
  then:
    // opional number of seconds. Default 1 second 
    conds.await( 5.0)

PollingConditions

Repeatedly evaluates one or more conditions until they are satisfied or a timeout has elapsed.

Optional constructor parameters:

  • initialDelay: Initial delay before first poll. Default 0 seconds
  • timeout: Failure timeout. Default 1 second
  • delay: Delay between evaluation polls. Default 0.1 seconds
  • factor: Backoff factor for delays. Default 1
    def conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25)
    def machine = new Machine()
  when:
    machine.start()
  then:
    // assert additional conditions within a time (secs)
    conditions.within( 2.0) {
      assert machine.temperature >= 100
    }
    // await the final set of conditions
    conditions.eventually {
      assert machine.efficiency >= 0.9
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment