How does GebSpec hide the field _browser and what is $spock_sharedField__browser good for? - groovy

I try to understand more about how Geb and Spock work internally to understand what is really happening in my tests.
I found that GebSpec which I extend to write my tests has a field Browser _browser.
I also found that GebSpec has a method getBrowser() which returns _browser, so _browser can be accessed over getBrowser() and get_browser(). But the interesting part is that while debugging in intelliJ expanding an instance of GebSpec shows no field _browser but only a field $spock_sharedField__browser.
A little example:
Debugging my Class: The instance of GebSpec has a field spock_sharedField__browser but no field _browser
How do they manage to hide the _browser field from me in the debugger and why do they do it?
Recall a field Browser _browser is declared in GebSpec and a field $spock_sharedField__browser is not.
There also is no method get$spock_sharedField__browser() but I still can access and manipulate $spock_sharedField__browser.
I tried to do it myself:
I wrote a class TestClass that declares _browser exactly analog to GebSpec, but if I debug here the field _browser is shown normally as one would expect
Can someone explain me what is going on?
Why hide _browser?
What is $spock_sharedField__browser good for?
UPDATE: I think the following code describes summarizes it pretty good:
import geb.spock.GebSpec
class GebHomeSpec extends GebSpec{
def "test Geb homepage"(){
when:
['get$spock_sharedField__browser', 'getBrowser', 'get_browser'].each {
try {
println this."${it}"()
} catch (MissingFieldException e) {
println e
}
}
['$spock_sharedField__browser', 'browser', '_browser'].each {
try {
println this.getMetaClass().getAttribute(this, it)
} catch (MissingFieldException e){
println e
}
}
then:
true
}
}
The result on the console is:
null
geb.Browser#352ff4da
geb.Browser#352ff4da
null
groovy.lang.MissingFieldException: No such field: browser for class: GebHomeSpec
groovy.lang.MissingFieldException: No such field: _browser for class: GebHomeSpec
My interpretation, considering the answer of kriegaex, is that in the Compilation during the Spock transformation the field $spock_sharedField__browser is declared and the field _browser is removed. The field browser never existed. But there are still getters for browser and _browser. I wonder where there get their data from (in this case geb.Browser#352ff4da) as none of the field exists anymore as the exceptions show. At least it matches with the debugging information (c.f. first picture/link) that shows the field $spock_sharedField__browser but neither a field _browser nor a field browser.
Finally I noticed (and I dont really know how to explain that) the getters for _browser and browser are outside of the class no longer available (see below). I thought the concept of private is not implemented in groovy and making getters private makes no sense to me anyways.
import geb.spock.GebSpec
class Main {
static void main(String[] args) {
GebSpec gebSpec = new GebSpec()
['get$spock_sharedField__browser', 'getBrowser', 'get_browser'].each {
try {
println gebSpec."${it}"()
} catch (MissingFieldException e) {
println e
}
}
['$spock_sharedField__browser', 'browser', '_browser'].each {
try {
println gebSpec.getMetaClass().getAttribute(gebSpec, it)
} catch (MissingFieldException e){
println e
}
}
}
}
This leads to
null
groovy.lang.MissingFieldException: No such field: $spock_sharedField__browser for class: org.codehaus.groovy.runtime.NullObject
groovy.lang.MissingFieldException: No such field: $spock_sharedField__browser for class: org.codehaus.groovy.runtime.NullObject
null
groovy.lang.MissingFieldException: No such field: browser for class: geb.spock.GebSpec
groovy.lang.MissingFieldException: No such field: _browser for class: geb.spock.GebSpec
All in all I find this rather confusing and I wonder what this is good for. Why introduce $spock_sharedField__browser and remove _browser?

If you use IntelliJ IDEA, you can just decompile the GebSpec class and will see something like this (this is what the Groovy compiler really produced when it compiled the library class):
public class GebSpec extends Specification implements GroovyObject {
// ...
#Shared
#FieldMetadata(
line = 29,
name = "_browser",
ordinal = 2
)
protected volatile Browser $spock_sharedField__browser;
// ...
public Browser createBrowser() {
CallSite[] var1 = $getCallSiteArray();
return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass() ? (Browser)ScriptBytecodeAdapter.castToType(var1[8].callConstructor(Browser.class, this.createConf()), Browser.class) : (Browser)ScriptBytecodeAdapter.castToType(var1[6].callConstructor(Browser.class, var1[7].callCurrent(this)), Browser.class);
}
public Browser getBrowser() {
CallSite[] var1 = $getCallSiteArray();
if (BytecodeInterface8.isOrigZ() && !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()) {
if (ScriptBytecodeAdapter.compareEqual(var1[11].callGroovyObjectGetProperty(this), (Object)null)) {
Browser var3 = this.createBrowser();
ScriptBytecodeAdapter.setGroovyObjectProperty(var3, GebSpec.class, this, (String)"_browser");
}
} else if (ScriptBytecodeAdapter.compareEqual(var1[9].callGroovyObjectGetProperty(this), (Object)null)) {
Object var2 = var1[10].callCurrent(this);
ScriptBytecodeAdapter.setGroovyObjectProperty((Browser)ScriptBytecodeAdapter.castToType(var2, Browser.class), GebSpec.class, this, (String)"_browser");
}
return (Browser)ScriptBytecodeAdapter.castToType(var1[12].callGroovyObjectGetProperty(this), Browser.class);
}
// ...
public Browser get$spock_sharedField__browser() {
return this.$spock_sharedField__browser;
}
public void set$spock_sharedField__browser(Browser var1) {
this.$spock_sharedField__browser = var1;
}
}
I think you have dived deep enough already to understand without further explanation.
Update: I forgot to mention: Your test class does not inherit GebSpec (which again inherits from Specification, i.e. the code will not be transformed by Spock/Geb because it has the wrong base class. If you do this, though:
package de.scrum_master.stackoverflow
import geb.spock.GebSpec
import spock.lang.Shared
class FooIT extends GebSpec {
#Shared
def myField
def test() {
expect:
true
}
}
Then the decompiled code will be:
package de.scrum_master.stackoverflow;
import geb.spock.GebSpec;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.spockframework.runtime.ErrorCollector;
import org.spockframework.runtime.SpockRuntime;
import org.spockframework.runtime.ValueRecorder;
import org.spockframework.runtime.model.BlockKind;
import org.spockframework.runtime.model.BlockMetadata;
import org.spockframework.runtime.model.FeatureMetadata;
import org.spockframework.runtime.model.FieldMetadata;
import org.spockframework.runtime.model.SpecMetadata;
import spock.lang.Shared;
#SpecMetadata(
filename = "FooIT.groovy",
line = 6
)
public class FooIT extends GebSpec {
#Shared
#FieldMetadata(
line = 7,
name = "myField",
ordinal = 0
)
protected volatile Object $spock_sharedField_myField;
public FooIT() {
CallSite[] var1 = $getCallSiteArray();
}
#FeatureMetadata(
line = 10,
name = "test",
ordinal = 0,
blocks = {#BlockMetadata(
kind = BlockKind.EXPECT,
texts = {}
)},
parameterNames = {}
)
public void $spock_feature_1_0() {
CallSite[] var1 = $getCallSiteArray();
ErrorCollector $spock_errorCollector = (ErrorCollector)ScriptBytecodeAdapter.castToType(var1[2].callConstructor(ErrorCollector.class, false), ErrorCollector.class);
ValueRecorder $spock_valueRecorder = (ValueRecorder)ScriptBytecodeAdapter.castToType(var1[3].callConstructor(ValueRecorder.class), ValueRecorder.class);
Object var10000;
try {
try {
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "true", Integer.valueOf(12), Integer.valueOf(5), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), true));
var10000 = null;
} catch (Throwable var13) {
SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "true", Integer.valueOf(12), Integer.valueOf(5), (Object)null, var13);
var10000 = null;
} finally {
;
}
var1[4].call(var1[5].call(this.getSpecificationContext()));
} finally {
$spock_errorCollector.validateCollectedErrors();
var10000 = null;
}
}
public Object get$spock_sharedField_myField() {
return this.$spock_sharedField_myField;
}
public void set$spock_sharedField_myField(Object var1) {
this.$spock_sharedField_myField = var1;
}
}
Update 2:
As for your additional questions, I can only speculate about the answers, I am sure users like #erdi (Geb maintainer), #Szymon Stepniak, #Leonard Brünings (who seem to be Groovy cracks, which I am not) could say more about it, but OTOH this is not a discussion forum and the questions are not particularly well suited for SO. Anyway, I edited the question tags to include "groovy" so as to maybe raise their attention.
Why introduce $spock_sharedField__browser and remove _browser?
I think it is just the result of Spock's way of transforming the #Shared annotation into a member variable with named so as to very unlikely collide with any existing member names. You also see this happening in the decompiled version of my own Spock/Geb specification.
But there are still getters for browser and _browser.
Of course there is a getter for browser, as in the Geb DLS you usually don't look behind the scenes but just use the syntactic sugar browser to access the browser instance. This Groovy-ism will call getBrowser(), as you probably know. This particular getter is declared explicitly in the GebSpec class in order to make the member conveniently accessible (you also see some lazy browser instantiation logic here):
Browser getBrowser() {
if (_browser == null) {
_browser = createBrowser()
}
_browser
}
I wonder where there get their data from (in this case geb.Browser#352ff4da) as none of the field exists anymore as the exceptions show.
I do not know enough about Groovy's dynamic language features to answer that, but you can see the actual mechanics in my decompiled code snippets.
Accessing Spock-specific class members from outside a running specification obviously does not work and probably is not meant to be. But if you run this test, it works just fine:
package de.scrum_master.stackoverflow
import geb.spock.GebSpec
import spock.lang.Shared
class FooIT extends GebSpec {
#Shared
def myField = "foo"
def test() {
given:
println browser
println myField
expect:
true
}
}
Console log:
geb.Browser#1722011b
foo

Related

Implicit assert statement in Groovy

Given I have JUnit tests written in Groovy:
class AssertTests {
#Test
void "explicit assert statement"() {
def value = 42
assert value == 100
}
#Test
void "no assert statement"() {
def value = 42
value == 100
}
}
When I execute them,
explicit assert statement test fails as expected thanks to assert statement.
no assert statement test passes and I would expect it to fail in a similar way how it's done when I use http://spockframework.org
How can I achieve implicit assert behavior for tests written in plain Groovy?
The answer is simple - you can't. Spock uses AST transformations to grab the code written in the then: part and transform it into the code that makes an equivalent of assertion (not the exact assert.)
To illustrate this, here is your test written in Spock:
import spock.lang.Specification
class TestSpec extends Specification {
def "should fail"() {
when:
def value = 42
then:
assert value == 100
}
}
And here is what its bytecode decompiled back to Java looks like:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import groovy.lang.GroovyObject;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.spockframework.runtime.ErrorCollector;
import org.spockframework.runtime.SpockRuntime;
import org.spockframework.runtime.ValueRecorder;
import org.spockframework.runtime.model.BlockKind;
import org.spockframework.runtime.model.BlockMetadata;
import org.spockframework.runtime.model.FeatureMetadata;
import org.spockframework.runtime.model.SpecMetadata;
import spock.lang.Specification;
#SpecMetadata(
filename = "TestSpec.groovy",
line = 5
)
public class TestSpec extends Specification implements GroovyObject {
public TestSpec() {
CallSite[] var1 = $getCallSiteArray();
super();
}
#FeatureMetadata(
line = 7,
name = "should fail",
ordinal = 0,
blocks = {#BlockMetadata(
kind = BlockKind.WHEN,
texts = {}
), #BlockMetadata(
kind = BlockKind.THEN,
texts = {}
)},
parameterNames = {}
)
public void $spock_feature_0_0() {
CallSite[] var1 = $getCallSiteArray();
ErrorCollector $spock_errorCollector = (ErrorCollector)ScriptBytecodeAdapter.castToType(var1[0].callConstructor(ErrorCollector.class, false), ErrorCollector.class);
ValueRecorder $spock_valueRecorder = (ValueRecorder)ScriptBytecodeAdapter.castToType(var1[1].callConstructor(ValueRecorder.class), ValueRecorder.class);
Object var10000;
try {
Object value = 42;
try {
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "value == 100", Integer.valueOf(12), Integer.valueOf(16), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), value), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), 100))));
var10000 = null;
} catch (Throwable var14) {
SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "value == 100", Integer.valueOf(12), Integer.valueOf(16), (Object)null, var14);
var10000 = null;
} finally {
;
}
var1[2].call(var1[3].call(this.getSpecificationContext()));
} finally {
$spock_errorCollector.validateCollectedErrors();
var10000 = null;
}
}
}
If you look at the SpockRuntime class, you will find that verifyCondition method checks if the condition found in the then: or and: block evaluates to true:
public static void verifyCondition(#Nullable ErrorCollector errorCollector, #Nullable ValueRecorder recorder,
#Nullable String text, int line, int column, #Nullable Object message, #Nullable Object condition) {
if (!GroovyRuntimeUtil.isTruthy(condition)) {
final ConditionNotSatisfiedError conditionNotSatisfiedError = new ConditionNotSatisfiedError(
new Condition(getValues(recorder), text, TextPosition.create(line, column), messageToString(message), null, null));
errorCollector.collectOrThrow(conditionNotSatisfiedError);
}
}
You can't avoid explicit assert in JUnit tests. You can hide them in some helper method, but you still need to call them. Keep in mind, that JUnit runner requires that the test method returns void, so you can't capture the result of a test method. (Replace void with def, and you will see that JUnit does not run your test.)
If you want to explore the world of compile-time metaprogramming in Groovy, you could experiment with writing your own AST transformations. Maybe there is a way to find boolean expression(s) and inject assert in front of it, but I can't guarantee you that it will work. If you look for an out-of-the-box solution for implicit asserts in Groovy JUnit tests, there is no such one.

Unexpected behaviour for Groovy 'with' method - variable assignment silently failed

I have the following code:
import groovy.transform.ToString
#ToString(includeNames = true)
class Simple {
String creditPoints
}
Simple simple = new Simple()
simple.with {
creditPoints : "288"
}
println simple
Clearly, I made a mistake here with creditPoints : "288". It should have been creditPoints = "288".
I expected Groovy to fail at the runtime saying that I made a mistake and I should have used creditPoints = "288"but clearly it did not.
Since it did not fail then what did Groovy do with the closure I created?
From the Groovy compiler perspective, there is no mistake in your closure code. The compiler sees creditPoints : "288" as labeled statement which is a legal construction in the Groovy programming language. As the documentation says, label statement does not add anything to the resulting bytecode, but it can be used for instance by AST transformations (Spock Framework uses it heavily).
It becomes more clear and easy to understand if you format code more accurately to the label statement use case, e.g
class Simple {
String creditPoints
static void main(String[] args) {
Simple simple = new Simple()
simple.with {
creditPoints:
"288"
}
println simple
}
}
(NOTE: I put your script inside the main method body to show you its bytecode representation in the next section.)
Now when we know how compiler sees this construction, let's take a look and see what does the final bytecode look like. To do this we will decompile the .class file (I use IntelliJ IDEA for that - you simply open .class file in IDEA and it decompiles it for you):
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import groovy.lang.Closure;
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import groovy.transform.ToString;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.InvokerHelper;
#ToString
public class Simple implements GroovyObject {
private String creditPoints;
public Simple() {
MetaClass var1 = this.$getStaticMetaClass();
this.metaClass = var1;
}
public static void main(String... args) {
Simple simple = new Simple();
class _main_closure1 extends Closure implements GeneratedClosure {
public _main_closure1(Object _outerInstance, Object _thisObject) {
super(_outerInstance, _thisObject);
}
public Object doCall(Object it) {
return "288";
}
public Object call(Object args) {
return this.doCall(args);
}
public Object call() {
return this.doCall((Object)null);
}
public Object doCall() {
return this.doCall((Object)null);
}
}
DefaultGroovyMethods.with(simple, new _main_closure1(Simple.class, Simple.class));
DefaultGroovyMethods.println(Simple.class, simple);
Object var10000 = null;
}
public String toString() {
StringBuilder _result = new StringBuilder();
Boolean $toStringFirst = Boolean.TRUE;
_result.append("Simple(");
if ($toStringFirst == null ? false : $toStringFirst) {
Boolean var3 = Boolean.FALSE;
} else {
_result.append(", ");
}
if (this.getCreditPoints() == this) {
_result.append("(this)");
} else {
_result.append(InvokerHelper.toString(this.getCreditPoints()));
}
_result.append(")");
return _result.toString();
}
public String getCreditPoints() {
return this.creditPoints;
}
public void setCreditPoints(String var1) {
this.creditPoints = var1;
}
}
As you can see, your closure used with the with method is represented as an inner _main_closure1 class. This class extends Closure class, and it implements GeneratedClosure interface. The body of the closure is encapsulated in public Object doCall(Object it) method. This method only returns "288" string, which is expected - the last statement of the closure becomes a return statement by default. There is no label statement in the generated bytecode, which is also expected as labels get stripped at the CANONICALIZATION Groovy compiler phase.

Is the #Repeatable annotation not supported by Groovy?

I'm coding in Groovy and am having trouble with the Java 8 #Repeatable meta-annotation. I think I'm doing everything right, but it appears that Groovy is not recognizing #Repeatable. Here's my sample code; I'm expecting the information from both annotations to get stored in MyAnnotationArray:
import java.lang.annotation.*
class MyClass
{
#MyAnnotation(value = "val1")
#MyAnnotation(value = "val2")
void annotatedMethod()
{
println("annotated method called")
}
public static void main(String... args)
{
MyClass ob = new MyClass()
ob.annotatedMethod()
java.lang.reflect.Method m = ob.getClass().getMethod("annotatedMethod")
List annos = m.getAnnotations()
println("annos = $annos")
}
}
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Repeatable(MyAnnotationArray)
public #interface MyAnnotation
{
String value() default "val0";
}
public #interface MyAnnotationArray
{
MyAnnotation[] MyAnnotationArray()
}
What happens is that I get this error:
Caught: java.lang.annotation.AnnotationFormatError: Duplicate annotation for class: interface MyAnnotation: #MyAnnotation(value=val2)
java.lang.annotation.AnnotationFormatError: Duplicate annotation for class: interface MyAnnotation: #MyAnnotation(value=val2)
Which is exactly what I get if I leave out the #Repeatable meta-annotation.
The code works fine if I leave out one of the duplicate MyAnnotations; then there is no error, and I then can read the annotation value as expected.
Is it possible that Groovy doesn't support the #Repeatable meta-annotation? I couldn't find any documentation that states this outright, though this page hints that maybe this is the case (scroll down to item 88).
seems to be not supported
i used java 1.8 and groovy 2.4.11
after compiling and de-compilig the same code i got this:
java:
#MyAnnotationArray({#MyAnnotation("val1"), #MyAnnotation("val2")})
public void annotatedMethod()
{
System.out.println("annotated method called");
}
groovy:
#MyAnnotation("val1")
#MyAnnotation("val2")
public void annotatedMethod()
{
System.out.println("annotated method called");null;
}
so, as workaround in groovy use
//note the square brackets
#MyAnnotationArray( [#MyAnnotation("val1"), #MyAnnotation("val2")] )
public void annotatedMethod()
{
System.out.println("annotated method called");
}
full script (because there were some errors in annotation declaration)
import java.lang.annotation.*
class MyClass
{
//#MyAnnotation(value = "val1")
//#MyAnnotation(value = "val2")
#MyAnnotationArray( [#MyAnnotation("val1"), #MyAnnotation("val2")] )
public void annotatedMethod()
{
System.out.println("annotated method called");
}
public static void main(String... args)
{
MyClass ob = new MyClass()
ob.annotatedMethod()
java.lang.reflect.Method m = ob.getClass().getMethod("annotatedMethod")
List annos = m.getAnnotations()
println("annos = $annos")
}
}
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Repeatable(MyAnnotationArray)
public #interface MyAnnotation
{
String value() default "val0";
}
#Retention(RetentionPolicy.RUNTIME)
public #interface MyAnnotationArray
{
MyAnnotation[] value()
}
also tried against groovy 3.0.0-SNAPSHOT - the result is the same as for 2.4.11
Yes, Groovy has supported "repeatable" annotations for a long time even in Java 5 so long as retention policy was only SOURCE. This is what allows multiple #Grab statements for instance without the outer #Grapes container annotation. Being only retained in SOURCE makes them useful for AST transformations and within the Groovy compiler itself (and other source processors) but not really useful anywhere else. We don't currently support #Repeatable at all but plan to in a future version.

Groovy: #Immutable results in Setters

I'm having trouble with (or I'm confused about) using #Immutability.
Given this class:
#Immutable
class TestField {
String key
}
I would expect I could not set a key on an instance like:
class TestFieldSpec extends Specification {
def 'do stuff'() {
when:
def t = new TestField('a')
println t
t.key = 'b'
println t
then:
t
}
}
However, when I run the test, I do not see a ReadOnlyPropertyException:
Running TestFieldSpec
TestField(a)
TestField(b)
This has been the case for me using both the groovy-eclipse-compiler and gmaven-plus with Maven.
Groovy version 2.4.7.
If I run javap on the class file created by both, I see:
public final void setKey(java.lang.String);
When I do the same in GroovyConsole though, things work how I'd expect them to.
#groovy.transform.Immutable
class TestField {
String key
}
def f = new TestField('a')
f.key = 'b'
Produces:
Exception thrown
groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: key for class: TestField
at TestField.setProperty(ConsoleScript1)
at ConsoleScript1.run(ConsoleScript1:7)
In the Groovy AST Browser, at Phase Finalization, I do not see the setter.
Any insight would be greatly appreciated.
Thanks!

Use Groovy Category implicitly in all instance methods of class

I have simple Groovy category class which adds method to String instances:
final class SampleCategory {
static String withBraces(String self) {
"($self)"
}
}
I want to use this category in my unit tests (for example). It looks like this:
class MyTest {
#Test
void shouldDoThis() {
use (SampleCategory) {
assert 'this'.withBraces() == '(this)'
}
}
#Test
void shouldDoThat() {
use (SampleCategory) {
assert 'that'.withBraces() == '(that)'
}
}
}
What I'd like to achieve, however, is ability to specify that category SampleCategory is used in scope of each and every instance method of MyTest so I don't have to specify use(SampleCategory) { ... } in every method.
Is it possible?
You can use mixin to apply the category directly to String's metaClass. Assign null to the metaClass to reset it to groovy defaults. For example:
#Before void setUp() {
String.mixin(SampleCategory)
}
#After void tearDown() {
String.metaClass = null
}
#Test
void shouldDoThat() {
assert 'that'.withBraces() == '(that)'
}
Now you have the option to use extension modules instead of categories:
http://mrhaki.blogspot.se/2013/01/groovy-goodness-adding-extra-methods.html
On the plus side Intellij will recognize the extensions. I've just noticed that it doesn't even need to be a separate module as suggested by the link, just add META-INF/services/org.codehaus.groovy.runtime.ExtensionModule to the project:
# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = module
moduleVersion = 1.0
extensionClasses = SampleExtension
The extension class is pretty much defined like a normal category:
class SampleExtension {
static String withBraces(String self) {
"($self)"
}
}
Can be used like:
def "Sample extension"() {
expect: 'this'.withBraces() == '(this)'
}
If you are using Spock there is a #Use annotation that can be used on the specifications. The drawback with that is that Intellij will not recognize it.

Resources