Testing with Spock

Specifications

Write Specifications describing Features (properties, aspects)
for a System under Specification (SUS) with Groovy 

import spock.lang.*

class AnySpecification extends Specification {

    // not shared between feature methods
    def obj = new SystemUnderSpecification()
    def col = new Collaborator()

    @Shared res = new VeryExpensiveResource()

    def setupSpec() {}    // runs once - before the first feature method
    def setup() {}        // runs before every feature method
    def cleanup() {}      // runs after every feature method
    def cleanupSpec() {}  // runs once - after the last feature method

    // feature methods

}

Feature Methods

  1. Set up the feature’s fixture

  2. Provide a stimulus to the system under specification

  3. Describe the response expected from the system

  4. Clean up the feature’s fixture

def "pushing an element on the stack"() {
    given:
    def stack = new Stack()
    def elem = "push me"

    when:
    stack.push(elem)

    then:
    !stack.empty
    stack.size() == 1
    stack.peek() == elem
}

What if ...

Condition not satisfied:

stack.size() == 2
|     |      |
|     1      false
[push me]
def "pushing an element on the stack"() {
    // [...]

    then:
    stack.size() == 2

    // [...]
}

Exception conditions

def "popping from an empty stack should throw exception"() {
    // [...]
    
    when:
    stack.pop()

    then:
    thrown(EmptyStackException)
}
def "popping from an empty stack should throw exception"() {
    // [...]
    
    when:
    stack.pop()

    then:
    def e = thrown(EmptyStackException)
    e.cause == null
}
def "HashMap accepts null key"() {
  given:
  def map = new HashMap()

  when:
  map.put(null, "elem")

  then:
  notThrown(NullPointerException)
}

Data Driven Testing

class MathSpec extends Specification {

  def "maximum of two numbers"() {
    expect:
    Math.max(1, 3) == 3
    Math.max(7, 4) == 7
    Math.max(0, 0) == 0
  }

  def "data driven maximum of two numbers"(int a, int b, int c) {
    expect:
    Math.max(a, b) == c

    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }

}

Detroit Style / Classic / State

  • use real objects if possible
  • use double if it's awkward to use the real thing

London Style / MOckist / Interaction

  • use a mock for any object with interesting behavior

Testing Variants

Mocking

class PublisherSpec extends Specification {
  
    Publisher publisher = new Publisher()
    Subscriber subscriber = Mock()
    Subscriber subscriber2 = Mock()

    def setup() {
        publisher.subscribers << subscriber
        publisher.subscribers << subscriber2
    }

    def "should send messages to all subscribers"() {
        when:
        publisher.send("hello")

        then:
        1 * subscriber.receive("hello")
        1 * subscriber2.receive("hello")
    }
}
class Publisher {

    List<Subscriber> subscribers = []
    int messageCount = 0

    void send(String message){
        subscribers*.receive(message)
        messageCount++
    }

}

interface Subscriber {
    void receive(String message)
}

Mocking is the act of describing (mandatory) interactions between the object under specification and its collaborators.

Stubbing

class PublisherSpec extends Specification {
  
    // [...]

    def "should send messages to all subscribers"() {
        given:
        subscriber.receive(_) >> "ok"
        // Mockito equivalent
        // when(entityManager.find(Customer.class,1L))
        //    .thenReturn(sampleCustomer);

        and:
        subscriber2.receive("message1") >> "ok"
        subscriber2.receive("message2") >> "fail"
        
        and:
        subscriber.receive(_) >> { String message ->
            message.size() > 3 ? "ok" : "fail"
        }
        
        // [...]
    }
}
interface Subscriber {
    String receive(String message)
}

Stubbing is the act of making collaborators respond to method calls in a certain way

Interaction based Testing

// Cardinality

  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

// Target Constraint

  1 * subscriber.receive("hello") // a call to 'subscriber'
  1 * _.receive("hello")          // a call to any mock object

// Method Constraint

  1 * subscriber.receive("hello") // a method named 'receive'
  1 * subscriber./r.*e/("hello")  // a method whose name matches the given regular expression

// Argument Constraints

  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({               // an argument that satisfies the given predicate
                   it.size() > 3 &&
                   it.contains('a') })
Spock JUnit
Specification Test class
setup() @Before
cleanup() @After
setupSpec() @BeforeClass
cleanupSpec() @AfterClass
Feature Test
Feature method Test Method
Data-driven feature Theory
Condition Assertion
Exception condition @Test(expected=…​)
Interaction Mock expectation (e.g. in Mockito)

Testing with Spock

By Peter Heisig

Testing with Spock

  • 501