JUnit 4
@Test
public void multiplicationOfZeroIntegersShouldReturnZero() {
// MyClass is tested
MyClass tester = new MyClass();
// Tests
assertEquals("10 x 0 must be 0", 0, tester.multiply(10, 0));
assertEquals("0 x 10 must be 0", 0, tester.multiply(0, 10));
assertEquals("0 x 0 must be 0", 0, tester.multiply(0, 0));
}
Spock
def "multiplication of zero integers should return zero"() {
given:
def tester = new MyClass()
expect:
0 == tester.multiply(10, 0)
0 == tester.multiply(0, 10)
0 == tester.multiply(0, 0)
}
Spock
@Unroll
def "multiply #left and #right should return zero"() {
given:
def tester = new MyClass()
expect:
0 == tester.multiply(left, right)
where:
left | right
10 | 0
0 | 10
0 | 0
}
What if there's an error?
List list = mock(List.class);
when(list.get(0)).thenReturn("one");
when(list.get(1)).thenReturn("two");
Spock
List list = Stub(List) {
get(0) >> "one"
get(1) >> "two"
}
For example, does your error handling behave gracefully?
service.remoteCall(_) >>>
["ok", "fail", "ok"] >>
{ throw new SocketException() } >>
"ok"
Groovy's closure -> method technique works as well
given:
def list = Stub(List)
list.get(_) >> {int idx -> idx == 0 ? "one" : idx == 1 ? "two" : "WAH? ${idx}"}
expect:
list.get(0) == "one"
list.get(1) == "two"
list.get(7) == "WAH? 7"
If you have cglib
in your classpath, you can mock classes (not just interfaces)
"Mock" can also function as a Stub and a Spy, but if you are doing that you should ask yourself why...
Spock transparently "appears" to be JUnit to tooling, so nothing special needs to be done in most tool-chains
Spock follows BDD terminology, so you write "feature descriptions" (analogous to tests) and "specifications" (analogous to test suites)
If you have ever used a more "pure" BDD platform, like RSpec, the difference between "feature description" vs "test" becomes pretty clear
However, because JUnit is the de-facto paradigm on the JVM, for both cultural and tooling reasons Spock specifications tend to look a lot more like JUnit+++ than RSpec
The easiest way to write a specification is to extend the Specification
class
class MyClassSpec extends Specification {
// ...
}
To create a feature definition create a method that defines what is "given", what happens "when" the feature is used, and what should "then" be true
def "this feature does ..."() {
given: // implicit if the code is not in another block, like "when"
def myClass = new MyClass()
myClass.initialize()
when:
def state = myClass.doesStuff()
then:
state.theNum == 12 // has implicit "assert" in front
}
Sometimes what you want to test is a simple function
def "this feature does ..."() {
expect: // combines "when" and "then"
Math.max(0, 3) == 3
}
The lines in "then" or "expect" should evaluate to true/false, including Groovy "truthiness"
"expect:
" is also useful for veryifying preconditions
@Shared handle = DBI.open("jdbc:h2:mem:test")
def "writes to the database when data is added"() {
expect:
selectInt("select count(*) from user") == 0 !
given:
def user = new User(name: "Spock") !
when:
userStore.persist(user)
then:
selectInt("select count(*) from user") == 1
}
Set up a variable for the features to use
class MyClassSpec extends Specification {
@Subject
MyClass myClass = new MyClass()
def setup() {
myClass.initialize()
}
def cleanup() {
myClass.close()
}
}
The @Subject
is informational only, just to highlight the "subject under test"
A new instance of MyClass
is created for each feature run
At compile time, the "new MyClass()
" is actually moved down into the "setup()
"
Set up a variable for the entire specification to use (and share between features)
class MyClassSpec extends Specification {
@Shared
MyClass myClass = new MyClass()
def setupSpec() {
myClass.initialize()
}
def cleanupSpec() {
myClass.close()
}
}
While a "static
" field could also work, @Shared
is much preferred because you have much better lifecycle control
static
fields should only be used for constants. (i.e., static final
)
When you want to check that an exception is thrown:
given:
crew << "Kirk"
when:
crew << "Picard"
then:
thrown TooManyCaptainsException
If you want to see the insides of the exception
given:
crew << "Kirk"
when:
crew << "Picard"
then:
def exp = thrown(TooManyCaptainsException)
exp.message == "Enterprise can only support one captain at a time, Picard."
exp.captain == "Picard"
Rather than manually tracking old state...
def stack = new Stack()
def "size increases when pushed"() {
when:
stack.push "bunny"
then:
stack.size() == old(stack.size()) + 1
}
That effectively gets rewritten to
def stack = new Stack()
def "size increases when pushed"() {
int old_stack_size = stack.size()
stack.push("bunny")
assert stack.size() == old_stack_size + 1
}
We saw some of this earlier:
@Unroll
def "multiply #left and #right should return zero"() {
given:
def tester = new MyClass()
expect:
0 == tester.multiply(left, right)
where:
left | right
10 | 0
0 | 10
0 | 0
}
External data source:
@Shared
@AutoCleanup
def sql = DBI.open("jdbc:h2:mem:test")
@Unroll
def "verify calculations for #a * #b == #c"() {
expect:
a * b == c
where:
[a, b, c] << sql.rows("select a, b, c from maxdata")
}
Making it easier to read, so that instead of
def "can tell if the string '#string' is an integer or not"() {
expect:
string.isInteger() == shouldBeInteger
where:
string | shouldBeInteger
"ABC" | false
"123" | true
"1.2" | false
"1 2" | false
"12a" | false
}
Use a simple calculation in "where"
def "the string '#string' is #description"() {
expect:
string.isInteger() == expected
where:
string | expected
"ABC" | false
"123" | true
"1.2" | false
"1 2" | false
"12a" | false
description = expected ? "an integer" : "not an integer"
}
For a lot more information, see the Data Driven Tests documentation
Ignoring a pending feature
class MyClassSpec extends Specification {
@Ignore("Want to record it while thinking about capabilities")
def "a feature that's pending"() {
expect:
false
}
def "a working feature"() {
expect:
true
}
}
@Ignore
can also be applied to the class to ignore the entire specification
There is a @PendingFeature
annotation that's much nicer for "not yet implemented" features, but it has not been released yet
For those times when you want to test just a single feature...
class MyClassSpec extends Specification {
def "a feature"() {
// ...
}
@IgnoreRest
def "another feature"() {
// ...
}
def "yet another feature"() {
// ...
}
}
Only "another feature" will run; the other features effectively have @Ignore
Like @Ignore
, this can also be applied to the class
Conditional features
@Requires({ os.windows })
def "I'll only run on Windows"() { ... }
@IgnoreIf({ jvm.java8 })
def "I'll run everywhere but on Java 8"() { ... }
Some of the properties provided are "os
", "sys
" (System.getProperty
), "env
" (environment variables), and "jvm
" (JVM info)
Especially for integration tests it's often useful to prevent a lot of false-positives to only run a test if you know the remote service is able to respond to events
@Requires({ remoteServiceIsUp() })
def "I'll only run if the remote service is up"() { ... }
static boolean remoteServiceIsUp() { ... }
While generally it's best if the order of execution of features are independant, sometimes you want/need to force their order
@Stepwise
class RunInOrderSpec extends Specification {
def "I run first"() { ... }
def "I run second"() { ... }
}
To set a timing threshold
@Timeout(10)
class TimedSpec extends Specification {
def "I fail after ten seconds"() { ... }
@Timeout(value = 250, unit = MILLISECONDS)
def "I fail much faster"() { ... }
}
Of course for "real" tests you'd want something much more robust/accurate, but this is simple
The following are the same
class MyClassSpec1 extends Specification {
@AutoCleanup
MyClass myClass = new MyClass()
}
class MyClassSpec2 extends Specification {
MyClass myClass = new MyClass()
def cleanup() {
myClass.close()
}
}
If the method to clean up your class' resources is something other than close()
, pass that as the value to @AutoCleanup
This also works on @Shared
members
Similar to th @Subject
annotation, another "documentation only" annotation that's useful is @Issue
@Issue("http://issuetracker.myco.com/ISSUE-14826")
def "should not have a regression bug anymore"() { ... }
For a lot more information, see the Extensions documentation