The Magic Behind the Spock

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 interetsed 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 expresions above are against Groovy syntax (in which Spock specifications are written).

  • ValueProvider valueProvider = Stub()
    Is a MethodCallExpression. Namely calling 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 - counstructs 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 Groovy compilator here, but a little against the Groovy runtime. Run the above Spec without the Spock magic and

So somewhere between a compilation and an execution must exist an extension point to which Spock reaches to perfom it’s 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 Groovy compiler transforms .groovy files into Java byte code, the overall work is divided into different phases, every of them performing different task. These phases can be found in 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 org.codehaus.groovy.transform.ASTTransformation file in 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 java doc 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 opposite to local transformation performed only on marked classes, e.g. by an annotation).
At the very beginning SpockTransform checks if given class is derived from Specification and if so, the tranformation process begins.

Spock steps in during SEMANTIC_ANALYSIS phase. Taking into consideration only the points we’re investigating, the AST representation of 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 the SpockTransform a lot is happening.

  • the above ClassNode (Node representing class in AST) is taken by SpecParser and turned into Spec - more convinient representation where among others - labeled statements (by given, when, then labels) are turned into blocks for every encountered feature method. (Check 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 tranformation is finished the AST tree will look like:
(it’s a simplified view, so the changes to original are more vivid - e.g. all annotations info is removed and complex subtrees are flatten 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 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

 

Written on February 27, 2017
comments powered by Disqus