Writing Assertions With Spock Framework

After we have finished this lesson, we:

  • Know how we can add conditions to our feature methods.
  • Can write assertions for the exceptions thrown by the system under specification.
  • Know how we can use Hamcrest matchers.

Introduction to Assertions (aka Conditions)

One difference between Spock Framework and JUnit is that Spock framework doesn’t provide an assertion API. Instead, we can specify the expected state by using conditions. We can write conditions by following these rules:

  • We have to specify the expected state in the then or expect block.
  • We can create conditions by using boolean expressions. To be more specific, we can use any expression and Groovy decides whether our expression is true or false.
  • If our condition is true, our test will pass.
  • If our condition is false, our test will fail.
If we want to specify conditions outside of the then and expect blocks, we have to use the assert keyword.

Additional Reading:

We can now create conditions with Spock Framework. Before we will put our new skills into practice, we will take a look at two rules which help us to write feature methods that are easy to read:

  • We should add the most important conditions to the beginning of the then or expect block and provide a good description to these conditions.
  • If our conditions belong to different logical groups, we should divide our then or expect block into different sections by using the and label. When we do this, we must provide a good description to the created sections.

Next, we will find out how we can add new conditions to the then and expect blocks.

Writing Assertions in the Then and Expect Blocks

As we already know, we can write assertions by adding new conditions to the then or expect block of our feature method. Let’s take a look at two examples that demonstrate how we can add conditions to our feature methods.

First, let’s write two conditions which verify that our feature method creates an empty list. We can do this by adding a then block to our feature method and adding our conditions to that block.

After we have added a then block to our feature method, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import spock.lang.Specification

@Category(UnitTest.class)
class SimpleConditionSpec extends Specification {

    def 'Write an assertion in the then block'() {

        when: 'We create a new list'
        def list = []

        then: 'Should create an empty list'
        list.isEmpty()
        list.size() == 0
    }
}
By the way, we don’t need both of these conditions. I used these conditions here because I wanted to demonstrate that we can use the return value of a method as a condition or specify the condition by using logical operators.

Second, let’s verify that the max() method of the Math class returns the greater of two numbers. We can do this by adding a new expect block to our feature method and adding our condition to that block.

After we have added a new expect block to our feature method, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import spock.lang.Specification

@Category(UnitTest.class)
class SimpleConditionSpec extends Specification {

    def 'Write an assertion in the expect block'() {

        expect: 'Should return the bigger number'
        Math.max(1, 2) == 2
    }
}
Additional Reading:

Let’s move on and find out how we can write assertions for the exceptions thrown by the system under specification.

Writing Assertions for Exceptions

When we want to verify that the when block of our feature method throws an exception, we have to use exception conditions. Next, we will take a look at three examples which demonstrate how we can write assertions for the exceptions thrown by the system under specification.

First, if we want to verify that the when block of our feature method throws a specific exception, we have to use the thrown() method and pass the type of the expected exception as a method parameter.

Let’s write a condition which verifies that the Stack class throws the EmptyStackException if we try to pop an empty stack. After we have written this condition, the source code of our feature method looks as follows:

import org.junit.experimental.categories.Category
import spock.lang.Specification

@Category(UnitTest.class)
class ExceptionConditionSpec extends Specification {

    def stack = new Stack()

    def 'Verify that exception is thrown'() {

        when: 'We try to remove an element from an empty stack'
        stack.pop()

        then: 'Should throw an exception'
        thrown EmptyStackException
    }
}

Second, if we want to verify that the when block of our feature method doesn’t throw a specific exception, we have to use the notThrown() method and pass the type of the exception as a method parameter.

Let’s write a condition which verifies that the Stack class doesn’t throw the EmptyStackException if we try to pop a stack that has one item. After we have written this condition, the source code of our feature method looks as follows:

import org.junit.experimental.categories.Category
import spock.lang.Specification

@Category(UnitTest.class)
class ExceptionConditionSpec extends Specification {

    def stack = new Stack()

    def 'Verify that exception is not thrown'() {

        given: 'The stack has one item'
        stack.push('Hello world!')

        when: 'We remove an element from the stack'
        stack.pop()

        then: 'Should not throw an exception'
        notThrown EmptyStackException
    }
}

Third, sometimes we want to write additional assertions for the thrown exception or we want to verify that the state of the system under specification is correct. Spock Framework supports both use cases because exception conditions can be followed by other conditions and other blocks.

Let’s ensure that our when block throws a RuntimeException that has the correct message. We can write the required conditions by following these steps:

  1. Verify that our when block throws a RuntimeException and get a reference to the thrown exception object.
  2. Ensure that the thrown exception has the correct message.

After we have written the required conditions, the source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import spock.lang.Specification

@Category(UnitTest.class)
class ExceptionConditionSpec extends Specification {

    def 'Write assertions for the thrown exception'() {

        when: 'A RuntimeException is thrown with a message'
        throw new RuntimeException('Hello World!')

        then: 'Should throw RuntimeException with the correct message'
        def ex = thrown RuntimeException
        ex.message == 'Hello World!'
    }
}

Next, we will find out how we can use Hamcrest matchers.

Using Hamcrest Matchers

Spock Framework allows us to specify conditions by using Hamcrest matchers. This is a good thing because it helps us to write conditions that are easy read, write, and maintain. When we want to write conditions by using a Hamcrest matcher, we have to use this format:

[object or value] [the used Hamcrest matcher]

Let’s verify that our feature method creates a list that has one item. We can write the required conditions by following these steps:

  1. Verify that the created list has one item.
  2. Ensure that the created list contains the correct item.

After we have written the required conditions, the source code of our feature method looks as follows:

import org.junit.experimental.categories.Category
import spock.lang.Specification

import static org.hamcrest.Matchers.contains
import static org.hamcrest.Matchers.hasSize

@Category(UnitTest.class)
class HamcrestConditionSpec extends Specification {

    def 'Write assertions with Hamcrest matchers'() {

        given: 'We create a new list'
        def list = []

        when: 'A new item is added to the list'
        list.add('Hello World!')

        then: 'The list should have one item'
        list hasSize(1)

        and: 'The list should contain the correct item'
        list contains('Hello World!')
    }
}

We can now add conditions to our feature methods, write assertions for the thrown exceptions, and specify conditions by using Hamcrest matchers.

Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us five things:

  • We should specify our conditions by using boolean expressions in the then or expect blocks.
  • We should add the most important conditions to the beginning of the then or expect block.
  • If our conditions belong to different logical groups, we should divide our block into different sections by using the and label.
  • Exception conditions can be followed by other conditions and other blocks.
  • If we want to use Hamcrest matchers, we have to specify our conditions by using the format: [object or value] [the used Hamcrest matcher].

Get the source code from Github