DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Related

  • Testing Asynchronous Operations in Spring With Spock and Byteman

Trending

  • How You Can Use Few-Shot Learning In LLM Prompting To Improve Its Performance
  • Modernizing Financial Systems: The Critical Role of Cloud-Based Microservices Optimization
  • Mastering Fluent Bit: Controlling Logs With Fluent Bit on Kubernetes (Part 4)
  • Four Essential Tips for Building a Robust REST API in Java
  1. DZone
  2. Coding
  3. Java
  4. Spock - Return Nested Spies / Mocks

Spock - Return Nested Spies / Mocks

By 
Marcin Grzejszczak user avatar
Marcin Grzejszczak
·
Aug. 08, 13 · Interview
Likes (1)
Comment
Save
Tweet
Share
16.1K Views

Join the DZone community and get the full member experience.

Join For Free

Hi! Some time ago I have written an article about Mockito and using RETURNS_DEEP_STUBS when working with JAXB. Quite recently we have faced a similliar issue with deeply nesetd JAXB and the awesome testing framework written in Groovy called Spock. Natively Spock does not support creating deep stubs or spies so we needed to create a workaround for it and this article will show you how to do it.

Project structure

We will be working on the same data structure as in the RETURNS_DEEP_STUBS when working with JAXB article so the project structure will be quite simillar:


As you can see the main difference is such that the tests are present in the /test/groovy/ folder instead of /test/java/ folder.

Extended Spock Specification

In order to use Spock as a testing framework you have to create Groovy test scripts that extend the Spock Specification class. The details of how to use Spock are available here. In this project I have created an abstract class that extends Specification and adds two additional methods for creating nested Test Doubles (I don't remember if I haven't seen a prototype of this approach somewhere on the internet).

ExtendedSpockSpecification.groovy

package com.blogspot.toomuchcoding.spock;
 
import spock.lang.Specification
 
/**
 * Created with IntelliJ IDEA.
 * User: MGrzejszczak
 * Date: 14.06.13
 * Time: 15:26
 */
abstract class ExtendedSpockSpecification extends Specification {
    /**
     * The method creates nested structure of spies for all the elements present in the property parameter. Those spies are set on the input object.
     *
     * @param object - object on which you want to create nested spies
     * @param property - field accessors delimited by a dot - JavaBean convention
     * @return Spy of the last object from the property path
     */
    protected def createNestedSpies(object, String property) {
        def lastObject = object
        property.tokenize('.').inject object, { obj, prop ->
            if (obj[prop] == null) {
                def foundProp = obj.metaClass.properties.find { it.name == prop }
                obj[prop] = Spy(foundProp.type)
            }
            lastObject = obj[prop]
        }
        lastObject
    }
 
    /**
     * The method creates nested structure of mocks for all the elements present in the property parameter. Those mocks are set on the input object.
     *
     * @param object - object on which you want to create nested mocks
     * @param property - field accessors delimited by a dot - JavaBean convention
     * @return Mock of the last object from the property path
     */
    protected def createNestedMocks(object, String property) {
        def lastObject = object
        property.tokenize('.').inject object, { obj, prop ->
            def foundProp = obj.metaClass.properties.find { it.name == prop }
            def mockedProp = Mock(foundProp.type)
            lastObject."${prop}" >> mockedProp
            lastObject = mockedProp
        }
        lastObject
    }
}

These two methods work in a very simillar manner.

  • Assuming that the method's argument property  looks as follows: "a.b.c.d" then the methods tokenize the string by "." and iterate over the array -["a","b","c","d"]. 
  • We then iterate over the properties of the Meta Class to find the one whose name is equal to prop (for example "a"). 
  • If that is the case we then use Spock's Mock/Spy method to create a Test Double of a given class (type). 
  • Finally we have to bind the mocked nested element to its parent. 
    • For the Spy it's easy since we set the value on the parent (lastObject = obj[prop]). 
    • For the mocks however we need to use the overloaded >> operator to record the behavior for our mock - that's why dynamically call the property that is present in the prop variable (lastObject."${prop}" >> mockedProp). 
  • Then we return from the closure the mocked/spied instance and we repeat the process for it

Class to be tested

Let's take a look at the class to be tested:

PlayerServiceImpl.java
package com.blogspot.toomuchcoding.service;
 
import com.blogspot.toomuchcoding.model.PlayerDetails;
 
/**
 * User: mgrzejszczak
 * Date: 08.06.13
 * Time: 19:02
 */
public class PlayerServiceImpl implements PlayerService {
    @Override
    public boolean isPlayerOfGivenCountry(PlayerDetails playerDetails, String country) {
        String countryValue = playerDetails.getClubDetails().getCountry().getCountryCode().getCountryCode().value();
        return countryValue.equalsIgnoreCase(country);
    }
}

The test class

And now the test class:

PlayerServiceImplWrittenUsingSpockTest.groovy
package com.blogspot.toomuchcoding.service
 
import com.blogspot.toomuchcoding.model.*
import com.blogspot.toomuchcoding.spock.ExtendedSpockSpecification
 
/**
 * User: mgrzejszczak
 * Date: 14.06.13
 * Time: 16:06
 */
class PlayerServiceImplWrittenUsingSpockTest extends ExtendedSpockSpecification {
 
    public static final String COUNTRY_CODE_ENG = "ENG";
 
    PlayerServiceImpl objectUnderTest
 
    def setup(){
        objectUnderTest = new PlayerServiceImpl()
    }
 
    def "should return true if country code is the same when creating nested structures using groovy"() {
        given:
            PlayerDetails playerDetails = new PlayerDetails(
                    clubDetails: new ClubDetails(
                            country: new CountryDetails(
                                    countryCode: new CountryCodeDetails(
                                            countryCode: CountryCodeType.ENG
                                    )
                            )
                    )
            )
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
    def "should return true if country code is the same when creating nested structures using spock mocks - requires CGLIB for non interface types"() {
        given:
            PlayerDetails playerDetails = Mock()
            ClubDetails clubDetails = Mock()
            CountryDetails countryDetails = Mock()
            CountryCodeDetails countryCodeDetails = Mock()
            countryCodeDetails.countryCode >> CountryCodeType.ENG
            countryDetails.countryCode >> countryCodeDetails
            clubDetails.country >> countryDetails
            playerDetails.clubDetails >> clubDetails
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
 
    def "should return true if country code is the same using ExtendedSpockSpecification's createNestedMocks"() {
        given:
            PlayerDetails playerDetails = Mock()
            CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode >> CountryCodeType.ENG
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
    def "should return false if country code is not the same using ExtendedSpockSpecification createNestedMocks"() {
        given:
            PlayerDetails playerDetails = Mock()
            CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode >> CountryCodeType.PL
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            !playerIsOfGivenCountry
    }
 
    def "should return true if country code is the same using ExtendedSpockSpecification's createNestedSpies"() {
        given:
            PlayerDetails playerDetails = Spy()
            CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode = CountryCodeType.ENG
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            playerIsOfGivenCountry
    }
 
    def "should return false if country code is not the same using ExtendedSpockSpecification's createNestedSpies"() {
        given:
            PlayerDetails playerDetails = Spy()
            CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
            countryCodeDetails.countryCode = CountryCodeType.PL
 
        when:
            boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
        then:
            !playerIsOfGivenCountry
    }
 
 
}
Let's move through the test methods one by one. First I present the code and then have a quick description of the presented snippet.
def "should return true if country code is the same when creating nested structures using groovy"() {
    given:
        PlayerDetails playerDetails = new PlayerDetails(
                clubDetails: new ClubDetails(
                        country: new CountryDetails(
                                countryCode: new CountryCodeDetails(
                                        countryCode: CountryCodeType.ENG
                                )
                        )
                )
        )
 
    when:
        boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
 
    then:
        playerIsOfGivenCountry
}

Here you could find the approach of creating nested structures by using the Groovy feature of passing properties to be set in the constructor.

def "should return true if country code is the same when creating nested structures using spock mocks - requires CGLIB for non interface types"() {
given:
PlayerDetails playerDetails = Mock()
ClubDetails clubDetails = Mock()
CountryDetails countryDetails = Mock()
CountryCodeDetails countryCodeDetails = Mock()
countryCodeDetails.countryCode >> CountryCodeType.ENG
countryDetails.countryCode >> countryCodeDetails
clubDetails.country >> countryDetails
playerDetails.clubDetails >> clubDetails
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}

Here you can find a test that creates mocks using Spock - mind you that you need CGLIB as a dependency when you are mocking non interface types.

def "should return true if country code is the same using ExtendedSpockSpecification's createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.ENG
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}

Here you have an example of creating nested mocks using the createNestedMocks method.

def "should return false if country code is not the same using ExtendedSpockSpecification createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.PL
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}


An example showing that creating nested mocks using the createNestedMocks method really works - should return false for improper country code.

def "should return true if country code is the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.ENG
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}


Here you have an example of creating nested spies using the createNestedSpies method.

def "should return false if country code is not the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.PL
when:
booleanplayerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}

An example showing that creating nested spies using the createNestedSpies method really works - should return false for improper country code.

Summary

In this post I have shown you how you can create nested mocks and spies using Spock. It can be useful especially when you are working with nested structures such as JAXB. Still you have to bear in mind that those structures to some extend violate the Law of Demeter. If you check my previous article about Mockito you would see that:
We are getting the nested elements from the JAXB generated classes. Although it violates the Law of Demeter it is quite common to call methods of structures because JAXB generated classes are in fact structures so in fact I fully agree with Martin Fowler that it should be called the Suggestion of Demeter.
And in case of this example the idea is the same - we are talking about structures so we don't violate the Law of Demeter.

Advantages

  • With a single method you can mock/spy nested elements
  • Code cleaner than creating tons of objects that you then have to manually set

Disadvantages

  • Your IDE won't help you with providing the property names since the properties are presented as Strings
  • You have to set Test Doubles only in the Specification context (and sometimes you want to go outside this scope)

Sources

As usual the sources are available at BitBucket and GitHub.

Spock (testing framework)

Published at DZone with permission of Marcin Grzejszczak, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Testing Asynchronous Operations in Spring With Spock and Byteman

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: