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.
Join the DZone community and get the full member experience.
Join For FreeSpock 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.
Published at DZone with permission of Dawid Kublik, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Cypress Tutorial: A Comprehensive Guide With Examples and Best Practices
-
JavaFX Goes Mobile
-
Why You Should Consider Using React Router V6: An Overview of Changes
-
Harnessing the Power of Integration Testing
Comments