The Spock testing and specification framework was released a few months ago, along with some wiki pages and surprisingly good documentation showing its usage.
The homepage greets you with a somewhat cryptic HelloSpock example:
Beyond the question of whether or not this is an innovation in testing, don't you wonder how the heck this works? Is that even a valid method name? Are those goto labels in there? Whaaaaat?def "can you figure out what I'm up to?"() {
expect:
name.size() == size
where:
name << ["Kirk", "Spock", "Scotty"]
size << [4, 5, 6]
}
Ignoring momentarily what makes it work, the 'expect:' block is going to be executed 3 times in this example: once with (Kirk, 4), once with (Spock, 5), and once with (Scotty, 6). You sort of have to read the 'where:' block horizontally to quickly see how the name and size variables relate to each other. Also, the variables name and size are somehow defined in the 'where:' block and then safely referenced from the 'expect:' block. So you're not only reading horizontally but also a little bottom to top. Maybe it'll help to rotate your monitor.
Oftentimes, javap is a good starting point to understand how things work. Here is the javap output (cleanup up a little) of the HelloSpock example:
I, for one, had no idea that question marks and spaces were legal method name characters in Java class files. But 'can you figure out what I'm up to?' in the class file proves it must be! So the specification method from the example is compiled to a method, and the class picks up a void setup() method... this isn't too surprising. There are a few other new mystery methods all starting with "__feature0". We'll get to those later. A bigger mystery is how the 'can you figure out what I'm up to?' method works at all. Check out how it looks in the AST Browser:$> javap HelloSpock
Compiled from "HelloSpock.groovy"
public class HelloSpock extends java.lang.Object implements groovy.lang.GroovyObject{
...
public void can you figure out what I'm up to?();
public void setup();
public java.lang.Object __feature0(java.lang.Object, java.lang.Object);
public java.lang.Object __feature0prov0();
public java.lang.Object __feature0prov1();
public java.lang.Object __feature0proc(java.lang.Object, java.lang.Object);
...
}
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmBgb5T2vXh35LuowN1yAjDH7XADDEB_KWfkGf-O30HiMpnhg7MKT290pHX953l7BCC5VFkmk7VBUSg5KsQ5-uEyRzsaglf88c918WLp42jGBIQugq6Gx2_G_xM_ODGkggCQZArPFJOrg/s400/shot0.png)
If you have trouble translating the AST into Groovy code then take a second look at the AST Browser and imagine how it might work.void "can you figure out what I'm up to?"() {
org.spockframework.runtime.SpockRuntime.featureMethodCalled()
}
Obviously, the code being executed is no longer in the method body where we first defined it. So where is it? The answer is those "__feature" methods. Check out what the debugger teaches you when the breakpoint is set within the 'expect:' block:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi8UOQbOHg5OVps21PE6dmRHR-t41I6eqyF_NC9N288shG-VEnKQaU0GIAC3wW0MnwmuMEtDppp0Bir5RW2NBh3KXxnfuYwQvIV6oX1bOQc_ej6bXCGPt94RYn7v01ESdJn_tNkFSO3JTM/s400/shot1.png)
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjTQSb2ouWsxlG-FmPdNTphhlC3AfxViw4LrW5xuKhKM-0R4ZeuiCrkIlIC1R1sAdJpEJBGD0NdgjIBE0qhUBGz7xJlTlKDSkQmW6sQKitQVC9sKH0Hqk_NgUwgJUw2HpZZCOwQ_uyYiZw/s400/shot2.png)
Hopefully, how Spock works is beginning to take shape. In the HelloSpock example, Spock is synthesizing 4 methods. The 'expect:' block is transformed into a runnable assertion method named __feature0 and the 'where:' blocks are accumulated and iterated, passing the test data into the assertion method one at a time. Using the debugger, putting a breakpoint in the 'where:' block reveals some interesting information. The name << ["Kirk", "Spock", "Scotty"] line of code is executed in the method __feature0prov0 and the size << [4, 5, 6] line is executed in __feature0prov1, despite being adjacent in the source file! Time to resort back to the AST Browser:
![](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7UWXhaL5NEJUF7D4l-UxNYUX_FCYZ9sX4165baZwA48QvItVVdxZD955nt-5Y_CYsdb8aKuhU1jY8swjgHWa3hABn5XDw9LVy8fnS6yqbQCaVwssIS7Nbo3Tp9VteQI9pre6zUU_CuNs/s400/shot3.png)
If you're paying attention, you'll realize that the name<< is the first line to execute, size<< the second, println the third, and the the 'expect:' block last. Executing top to bottom? That's a pretty good trick. As for the logging, perhaps there is a way better way to do this... but you can't just put a println in the 'where:' block because Spock will try to bind it as a variable. Luckily, there aren't really void methods in Groovy; methods just return null instead. So setting ignored equal to a println result is totally valid, as long as you're willing to put up with a null 'ignored' variable in scope for your 'expect:' block. Anyway (p0, p1) is (Kirk, 4) as you'd expect, along with the other combinations later.where:
ignored = println ("where: \n p0=$p0\n p1=$p1")
name << ["Kirk", "Spock", "Scotty"]
size << [4, 5, 6]
So that's it. That's how the goto hijacking of Spock specifications works. The method called "can you figure out what I'm up to?" is almost totally discarded and replaced within the test runner. Each 'where:' block variable is transformed into a separate method call named __feature0__provN, and the 'expect:' block is transformed into a method that takes the input generated and runs the assertions. The __feature0proc method worries about moving data from the 'where:' blocks into a format the 'expect:' block can understand.
That is pretty cool. I wonder if it actually helps you write better tests.
1 comment:
Post a Comment