Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Magic Behind the Spock

DZone's Guide to

The Magic Behind the Spock

If you're using Spock to write your tests, it's important to know how its syntax works and how its Abstract Syntax Tree handles transformations.

· Java Zone
Free Resource

Build vs Buy a Data Quality Solution: Which is Best for You? Gain insights on a hybrid approach. Download white paper now!

Spock rulez. Writing tests has never been simpler and more pleasant. All thanks to concise and informative syntax. But how is Spock syntax made possible?

Expressions like

  • ValueProvider valueProvider = Stub()
  • valueProvider.provideValue() » 21
  • then:

What makes them work?

If you’re interested follow my plan which is to focus on simple Spec like:

class MagnifyingProxySpec extends Specification {

    ValueProvider valueProvider = Stub()

    @Subject
    MagnifyingProxy magnifyingProxy = new MagnifyingProxy(valueProvider)

    def "should magnify value"() {
        given:
            valueProvider.provideValue() >> 21
        when:
            int result = magnifyingProxy.provideMagnifiedValue()
        then:
            result == 210
    }
}


And a present high-level view of what makes it tick. 

It All Compiles

Let’s start with noticing that none of the expressions above are against Groovy syntax (in which Spock specifications are written).

  • ValueProvider valueProvider = Stub()

 Is a MethodCallExpression. Namely, calling the Stub() method and assigning it to a valueProvider variable (method name starting with capital letter is only a disguise).

  • valueProvider.provideValue() » 21 

Is a BinaryExpression using » operator on valueProvider.provideValue() and 21

  • then:

And when: and given: are Groovy labels — constructs that (repeating after doc) have no impact on the semantics of the code but can be used to make the code easier to read.

So nothing against the Groovy compiler here, but a little against the Groovy runtime. Run the above Spec without the Spock magic and:

  • Stub()

 will throw InvalidSpecException as this is as it is defined in the MockingApi class.

  • » 

will be called to DefaultGroovyMethods.rightShift(Number self, Number operand) and not stubbing a method call.

  • then: 

will just be a label with no extra functionality like asserting every comparison

So somewhere between a compilation and an execution, there must exist an extension point to which Spock reaches to perform its tricks.

Groovy Compiler and AST Representation

Remember the MethodCallExpression and BinaryExpression terms I used above? These are just two elements of Groovy source code representation as the Abstract Synax Tree (AST). The idea is to represent every block of code by some subtree of ASTNodes.

The AST abstraction is used in the compilation process. When the Groovy compiler transforms .groovy files into Java bytecode, the overall work is divided into different phases, every of them performing a different task. These phases can be found in the Phases class

public static final int INITIALIZATION        = 1;   // Opening of files and such
public static final int PARSING               = 2;   // Lexing, parsing, and AST building
public static final int CONVERSION            = 3;   // CST to AST conversion
public static final int SEMANTIC_ANALYSIS     = 4;   // AST semantic analysis and elucidation
public static final int CANONICALIZATION      = 5;   // AST completion
public static final int INSTRUCTION_SELECTION = 6;   // Class generation, phase 1
public static final int CLASS_GENERATION      = 7;   // Class generation, phase 2
public static final int OUTPUT                = 8;   // Output of class to disk
public static final int FINALIZATION          = 9;   // Cleanup
public static final int ALL                   = 9;   // Synonym for full compilation


Basically, the whole process built from these phases can be summarized as:

  • read data from some input (source file, String script, etc)
  • transform it to more robust representation — AST (Abstract Syntax Tree)
  • complement and transform the AST representation
  • generate GroovyClass from AST
  • write GroovyClass as a .class file

The crucial point here is that the AST is exposed not only for Groovy’s internal usage, but also for user-defined external AST transformations. And this is where the Spock comes in.

Spock AST Transformation

When CompilationUnit is first created by GroovyClassLoader, it collects all global transformations it can find in META-INF/services/org.codehaus.groovy.transform.ASTTransformation files.
Open the org.codehaus.groovy.transform.ASTTransformation file in the spock-core JAR, and you’ll find:

org.spockframework.compiler.SpockTransform 


Which is the Spock AST transformation, being the entry point for the whole process, as we can read in the Javadoc above the class:

/**
 * AST transformation for rewriting Spock specifications. Runs after phase
 * SEMANTIC_ANALYSIS, which means that the AST is semantically accurate
 * and already decorated with reflection information. On the flip side,
 * because types and variables have already been resolved,
 * program elements like import statements and variable definitions
 * can no longer be manipulated at will.
 *
 * @author Peter Niederwieser
 */
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class SpockTransform implements ASTTransformation


SpockTransform is a global transformation, meaning it will be executed once for every Groovy class being compiled (as opposed to local transformations performed only on marked classes, e.g. by an annotation).

At the very beginning, SpockTransform checks if a given class is derived from Specification, and if so, the transformation process begins.

Spock steps in during the SEMANTIC_ANALYSIS phase. Taking into consideration only the points we’re investigating, the AST representation of the MagnifyingProxySpec class before Spock transformation can be simplified to:

ClassNode: MagnifyingProxySpec
  fields:
    [0]: name: valueProvider
         type: ValueProvider -> ValueProvider
         initialValueExpression:
             MethodCallExpression: Stub()
    [1]: name: magnifyingProxy
         type: MagnifyingProxy -> MagnifyingProxy
         initialValueExpression:
             ConstructorCallExpression: new MagnifyingProxy(valueProvider)
  methods:
    name: "should magnify value"
    code:
      statements:
        [0]: expresion: 
               BinaryExpression:
                 leftExpresion:  valueProvider.provideValue() 
                 rightExpresion: 21
                 operation: >>
             statementLabels: given
        [1]: expresion: 
               DeclarationExpression:
                 leftExpresion: int result 
                 rightExpresion: magnifyingProxy.provideMagnifiedValue()
                 operation: =
             statementLabels: when
        [2]: expresion:
               BinaryExpression:
                 leftExpresion: result
                 rightExpresion: 210
                 operation: == 
             statementLabels: then


Rewriting the AST

During SpockTransform , a lot is happening.

  • The above ClassNode (Node representing the class in AST) is taken by SpecParser and turned into Spec — a more convenient representation where, among others, labeled statements (by given, when, then labels) are turned into blocks for every encountered feature method. (Check the BlockParseInfo class and you will see what labels are allowed)
  • The same ClassNode is being rewritten by SpecRewriter — this is where most of the AST transformation is happening
  • As the AST is rewritten, some information about original structure still needs to be kept — these are added to the new AST structure in a form of annotations by SpecAnnotator

After the transformation is finished, the AST tree will look like below. It’s a simplified view, so the changes to the original are more vivid — e.g. all annotation info is removed and complex subtrees are flattened to String values):

ClassNode: MagnifyingProxySpec
  fields:
    [0]: name: valueProvider
         type: ValueProvider -> ValueProvider
         initialValueExpression: null
    [1]: name: magnifyingProxy
         type: MagnifyingProxy -> MagnifyingProxy
         initialValueExpression: null
  methods:
    [0]: name: "$spock_initializeFields"
         code:
           statements:
             [0]: expresion: 
                    FieldInitializationExpression:
                        leftExpresion:  valueProvider
                        rightExpresion: StubImpl('valueProvider', ValueProvider)
                        operation: =
             [1]: expresion: 
                    FieldInitializationExpression:
                        leftExpresion: magnifyingProxy
                        rightExpresion: new MagnifyingProxy(valueProvider)
                        operation: =
    [1]: name: "$spock_feature_0_0"
         code:
           statements:
             [0]: expresion: 
                    DeclarationExpression:
                        leftExpresion:  $spock_valueRecorder
                        rightExpresion: new ValueRecorder()
                        operation: =
             [1]: expresion: 
                    MethodCallExpression: this.getSpecificationContext().getMockController().addInteraction(new InteractionBuilder(14, 13, 'valueProvider.provideValue() >> 21').addEqualTarget(valueProvider).addEqualMethodName('provideValue').setArgListKind(true).addConstantResponse(21).build())
             [2]: expresion:
                    DeclarationExpression:
                        leftExpresion: Integer result
                        rightExpresion: magnifyingProxy.provideMagnifiedValue()
                        operation: = 
             [3]: expresion:
                    MethodCallExpression: SpockRuntime.verifyCondition($spock_valueRecorder.reset(), 'result == 210', 20, 13, null, $spock_valueRecorder.record(2, $spock_valueRecorder.record(0, result) == $spock_valueRecorder.record(1, 210)))
             [4]: expresion:
                    MethodCallExpression: getSpecificationContext().getMockController().leaveScope()


Instead of describing it, it would be better to compare the source code from Groovy AST Browser before and after the SpockTransform transformation:

Before:

public class MagnifyingProxySpec extends Specification { 

    private ValueProvider valueProvider 
    @Subject
    private MagnifyingProxy magnifyingProxy 

    public Object should magnify value() {
        valueProvider.provideValue() >> 21
        Integer result = magnifyingProxy.provideMagnifiedValue()
        result == 210
    }
}


After:

@SpecMetadata(filename = 'script1488219488396.groovy', line = 5)
public class MagnifyingProxySpec extends Specification { 

    @FieldMetadata(line = 7, name = 'valueProvider', ordinal = 0)
    private ValueProvider valueProvider 
    @Subject
    @FieldMetadata(line = 9, name = 'magnifyingProxy', ordinal = 1)
    private MagnifyingProxy magnifyingProxy 

    private Object $spock_initializeFields() {
        valueProvider = this.StubImpl('valueProvider', ValueProvider)
        magnifyingProxy = new MagnifyingProxy(valueProvider)
    }

    @FeatureMetadata(line = 12, blocks = [
      []org.spockframework.runtime.model.BlockKind.SETUPorg.codehaus.groovy.ast.AnnotationNode@138c8ce,
      []org.spockframework.runtime.model.BlockKind.WHENorg.codehaus.groovy.ast.AnnotationNode@7e7261e6,
      []org.spockframework.runtime.model.BlockKind.THENorg.codehaus.groovy.ast.AnnotationNode@34e6c430],
      name = 'should magnify value', parameterNames = [], ordinal = 0)
    public void $spock_feature_0_0() {
        Object $spock_valueRecorder = new ValueRecorder()
        this.getSpecificationContext().getMockController()
            .addInteraction(
                new InteractionBuilder(14, 13, 'valueProvider.provideValue() >> 21')
                    .addEqualTarget(valueProvider)
                    .addEqualMethodName('provideValue')
                    .setArgListKind(true)
                    .addConstantResponse(21)
                    .build())
        Integer result = magnifyingProxy.provideMagnifiedValue()
        SpockRuntime.verifyCondition(
            $spock_valueRecorder.reset(), 'result == 210', 20, 13, null,
            $spock_valueRecorder.record(2, $spock_valueRecorder.record(0, result) == $spock_valueRecorder.record(1, 210)))
        this.getSpecificationContext().getMockController().leaveScope()
    }
}


From the code above, it should be clear that

  • Stub() 

Exception-throwing method has been replaced by StubImpl() from SpecInternals class

  • valueProvider.provideValue() » 21 

Stubbing was replaced with MockController interaction

  • then: 

Block was transformed to condition verification.

If you want to read more about AST transformations (among others) check the great Groovy metaprogramming guide.

Build vs Buy a Data Quality Solution: Which is Best for You? Maintaining high quality data is essential for operational efficiency, meaningful analytics and good long-term customer relationships. But, when dealing with multiple sources of data, data quality becomes complex, so you need to know when you should build a custom data quality tools effort over canned solutions. Download our whitepaper for more insights into a hybrid approach.

Topics:
spock ,groovy ,groovy ast transformations ,java ,tutorial

Published at DZone with permission of Dawid Kublik, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}