Creating Stubs With Spock Framework - Introduction and Argument Constraints

After we have finished this lesson, we

  • Know how we can create stubs with Spock Framework.
  • Can configure the expected method parameters by using argument constraints.

Introduction to System Under Specification

During this lesson, we will create a MessageRepository stub and use this stub when we specify the expected behavior of the MessageService class.

The MessageRepository interface declares one method called findById() that takes the id of the message as a method parameter. This method returns an Optional object that contains the found Message object. If no message is found, this method returns an empty Optional object.

The source code of the MessageRepository interface looks as follows:

1import java.util.Optional;
2
3interface MessageRepository {
4
5    Optional<Message> findById(Long id);
6}

The Message class contains the information of a single message. Its source code looks as follows:

1public class Message {
2
3    private Long id;
4    private String messageText;
5
6    public Message() {}
7
8    //Getters and setters are omitted
9}

The MessageService class uses the MessageRepository object, which is given as a constructor argument, when it communicates with the used database. It has one method called findById() that takes the id of the requested message as a method parameter. This method invokes the findById() method of the MessageRepository interface and returns the found message. If no message is found, this method throws a NotFoundException.

The source code of the MessageService class looks as follows:

 1public class MessageService {
 2
 3    private final MessageRepository repository;
 4
 5    public MessageService(MessageRepository repository) {
 6        this.repository = repository;
 7    }
 8
 9    public Message findById(Long id) {
10        return repository.findById(id).orElseThrow(
11                () -> new NotFoundException(String.format(
12                        "No message found with id: %d",
13                        id
14                ))
15        );
16    }
17}

Next, we will find out how we can create stubs with Spock Framework.

Creating Stubs With Spock Framework

We can create a stub with Spock Framework by invoking the Stub() method of the MockingApi class. This method takes the type of the created stub as a method parameter and returns the created stub object.

All stubs created with Spock Framework allow unexpected invocations. The return value of such invocation is selected by following these rules:

  • If the return value is a primitive type, our stub returns the primitive type’s default value.
  • If the return value is non-primitive numerical value such as a BigDecimal, our stub returns zero.
  • If the return value is a non-numerical value, our stub returns an “empty” or “dummy” object such as an empty String, an empty collection, or an object constructed by using its default constructor.

When we create new stubs with Spock Framework, we must store the created stubs in normal instance fields. This ensures that every feature method gets its own stub object. In other words, the configuration done in a feature method doesn’t affect other feature methods.

Let’s create a MessageRepository stub and use this stub when we create a new MessageService object. After we have configured the system under specification, the source code of our specification class looks as follows:

1import org.junit.experimental.categories.Category
2import spock.lang.Specification
3
4@Category(UnitTest.class)
5class MessageServiceSpec extends Specification {
6
7    def repository = Stub(MessageRepository)
8    def service = new MessageService(repository)
9}
If the system under specification (aka MessageService) requires setup code that is longer than one line, we should configure it in the setup() method because (IMO) it makes our specification class a bit cleaner.

After we have configured the system under specification, we are finally ready to use our new stub. Let’s find out how we can identify the stubbed method and configure the expected method parameters.

Using Argument Constraints

We can identify the stubbed method (aka expected interaction) by using the syntax that we normally use when we invoke a method. However, identifying the stubbed method is not good enough. We have to also configure the expected method parameters. We can do this by using so called argument constraints.

If the stubbed method has multiple parameters, we have to use commas to separate our argument constraints.

Next, we will find out how we can use the argument constraints supported by Spock Framework.

An Argument Is Equal To

If we want to create an argument constraint which ensures that the expected method parameter is equal to a value or an object, we can simply use the expected value or object as an argument constraint.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using a method parameter that is equal to 1L.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with id that is equal to expected id'() {
11
12        given: 'A message is found with an id that is equal to 1'
13        def found = new Message()
14        repository.findById(1L) >> Optional.of(found)
15
16        when: 'We find a message by using the id 1'
17        def returned = service.findById(1L)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}
We can use the the right-shift (>>) operator when we want to return a fixed value. We will talk more about this in the next lesson of this topic .

An Argument Is Not Equal To

If we want to create an argument constraint which ensures that the expected method parameter is not equal to a value or an object, we can create the argument constraint by using the logical not operator (!) together with the value or object.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using a method parameter that is not equal to 2L.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with id that is not equal to configured id'() {
11
12        given: 'A message is found with an id that is not equal to 2'
13        def found = new Message()
14        repository.findById(!2L) >> Optional.of(found)
15
16        when: 'We find a message by using the id 1'
17        def returned = service.findById(1L)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}

An Argument Can Be Anything (Including Null)

If we want to create an argument constraint which ensures that the expected method parameter can be anything, we can use the underscore character (_) as an argument constraint.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using any single method parameter.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with any single argument (includes null)'() {
11
12        given: 'A message is found with any single argument'
13        def found = new Message()
14        repository.findById(_) >> Optional.of(found)
15
16        when: 'We find a message by using null id'
17        def returned = service.findById(null)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}
It’s important to understand that this argument constraint accepts null values as well.

Any Argument List (Including an Empty List)

If we want to create an argument constraint which ensures that any argument list can be used when the stubbed method is invoked, we can use the star character together with the underscore character (*_) as an argument constraint.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using any argument list.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with any argument list (includes empty list)'() {
11
12        given: 'A message is found when any argument list is given'
13        def found = new Message()
14        repository.findById(*_) >> Optional.of(found)
15
16        when: 'We find a message by using the id 1'
17        def returned = service.findById(1L)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}

This argument constraint accepts an empty argument list as well. If you want to create an argument constraint that accepts only an empty argument list, you have to use the following format:

1repository.findById()

An Argument Is Not Null

If we want to create an argument constraint which ensures that the expected method parameter is not null, we can use the logical not operator (!) together with the null keyword as an argument constraint.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using a method parameter that is not null.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with any non-null argument'() {
11
12        given: 'A message is found with any non-null id'
13        def found = new Message()
14        repository.findById(!null) >> Optional.of(found)
15
16        when: 'We find a message by using the id 1'
17        def returned = service.findById(1L)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}

An Argument of Specific Type Is Not Null

If we want to create an argument constraint which ensures that the type of expected method parameter is X and it is not null, we have to create our argument constraint by using the following format: _ as X.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using a non-null method parameter that is a Long object.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with any non-null Long argument'() {
11
12        given: 'A message is found with any non-null id that is Long'
13        def found = new Message()
14        repository.findById(_ as Long) >> Optional.of(found)
15
16        when: 'We find a message by using the id 1'
17        def returned = service.findById(1L)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}

An Argument That Satisfies the Given Predicate

If we want to create an argument constraint which ensures that the expected method parameter satisfies the given predicate, we can create our argument constraint by using a closure that specifies the predicate.

Let’s create an argument constraint which ensures that the findById() method of the MessageRepository interface should be invoked by using the method parameter 1L.

After we have created this argument constraint, the relevant part of our specification class looks as follows:

 1import org.junit.experimental.categories.Category
 2import spock.lang.Specification
 3
 4@Category(UnitTest.class)
 5class MessageServiceSpec extends Specification {
 6
 7    def repository = Stub(MessageRepository)
 8    def service = new MessageService(repository)
 9
10    def 'Get message with an argument that satisfies a predicate'() {
11
12        given: 'A message is found with id that satisfies the given predicate'
13        def found = new Message()
14        repository.findById({it == 1L}) >> Optional.of(found)
15
16        when: 'We find a message by using the id 1'
17        def returned = service.findById(1L)
18
19        then: 'Should return the found message'
20        returned == found
21    }
22}

We can now specify the expected method parameters by using argument constraints. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us five things:

  • We can create a stub by using the Stub() method of the MockingApi class.
  • All stubs created with Spock Framework allow unexpected invocations.
  • We should store our stubs in normal instance fields because this ensures that the configuration done in a feature method cannot affect other feature methods.
  • We can configure the expected method parameters by using argument constraints.
  • We can implement custom argument constraints by using closures.

Get the source code from Github