I'd always use JUnit as the testing framework, a good library and pretty much the standard for testing in Java. However, sometimes you can feel something is missing.
In fact, this feeling is well described in the article Introducing BDD by Dan North (considered the father of BDD):
I had a problem. While using and teaching agile practices like test-driven development (TDD) on projects in different environments, I kept coming across the same confusion and misunderstandings. Programmers wanted to know where to start, what to test and what not to test, how much to test in one go, what to call their tests, and how to understand why a test fails.
There's no doubt that TDD is a useful tool in software development. But more than testing, TDD is about improving the design process.
Of course, writing tests gives you the confidence to know that the system works correctly. However, more important than that, by writing tests before code the behavior of the system is well defined and documented.
This is not always seen, so the aim of Behaviour-Driven Development (BDD) is to make us realize the importance of defining behavior rather than testing per se by using a specific vocabulary to minimize misunderstandings and encourage the collaboration between business analysts, developers, testers, and stakeholders. For this reason, BDD can be considered an extension of TDD.
In BDD, testing and development are based on that common language and the basic unit to describe a feature of the system is a story. Acceptance criteria (the base of tests) are part of the story.
In his article What's in a story?, Dan North suggests the following template for a story:
1Title (one line describing the story)
2
3Narrative:
4As a [role]
5I want [feature]
6So that [benefit]
7
8Acceptance Criteria: (presented as Scenarios)
9
10Scenario 1: Title
11Given [context]
12And [some more context]...
13When [event]
14Then [outcome]
15And [another outcome]...
16
17Scenario 2: ...
For example:
1Story: Calculate amount to pay for a service (including tip)
2
3As a member of a group
4I want to know the amount to pay including tip
5So that I can know how much I have to pay
6
7Scenario 1: Service was acceptable
8Given the amount of the bill $100
9 And the percentage to tip 15%
10 And the number of people 2
11When the tip is calculated
12Then the tip amount should be $15
13 And the total to pay should be $115
14 And the total per person is $57.50
Based on this template, Dan North developed JBehave, a Java framework that supports BDD and maps the steps of a scenario to Java methods via annotations.
JBehave inspired other frameworks like RSpec, which in turn, inspired the Spock framework, that in my opinion, brings testing to another level. You're going to see why.
Spock is a testing framework writing in Groovy, which means that runs on the Java Virtual Machine (JVM) and let you test code written in any of the languages the JVM supports.
In this page you'll read ten reasons why you might use Spock. Here, I just want to highlight three of them:
But enough talk. Let get to the code.
For this tutorial, we are going to use Eclipse and Maven to test a class in Java using Groovy and Spock.
Why not use IntelliJ IDEA and Gradle? Because setting up Eclipse and Maven is a little more complicated, and at least where I come from, more folks are using these tools.
For Eclipse, first, we need to install the Groovy plugin and some dependencies. Then, we'll create a Maven project, configure all the required dependencies and plugins in the pom.xml
file and add a directory where the testing classes will live.
Let's start. I'll assume you're using Eclipse Mars.2 (version 4.5.2, but I shouldn't be very different if you're using a different version), so open Eclipse and to Help -> Install new software.
Click the Add... button
Enter the following information and press OK (if you're using another version of Eclipse, you can find the correct link to the update site here):
Name: Groovy plugin (or any name you want)
Wait for it to fetch the information and then select the Groovy-Eclipse and Extra Groovy Compilers options and click Next.
Review the items to install and click Next.
Accept the terms of the license and click Finish.
Wait for a while and Eclipse will install the Groovy plugin. You will need to restart Eclipse.
Now let's create the project. Go to File -> New -> Other... and choose Maven Project (notice that thanks to the Groovy plugin, you now have the options to create a Groovy class and a Groovy project)
In the next window, check the option Create a simple project (skip archetype selection) and click Next.
In this window, enter the project information, For instance:
Group ID: com.example
Artifact ID: spock
Version: 0.0.1-SNAPSHOT
Packaging: jar
This will create a project with a pom.xml
file and the following directories:
1src/main/java
2src/main/resources
3src/test/java
4src/test/resources
However, since we're going to use Groovy, the right thing to do is create an src/test/groovy
directory. To do so, right-click on the name of the project and choose the option Build Path -> Configure Build Path and then the Source tab.
Click Add Folder... and create the new folder (relative to src/test
you should only enter groovy) without inclusion or exclusion patterns.
Next, select the new directory's option Output Folder and click Edit.
To specify the output directory for tests (target/test-classes
) and click OK.
Regarding the pom.xml
file, it should look like this:
1<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3 <modelVersion>4.0.0</modelVersion>
4
5 <groupId>com.example</groupId>
6 <artifactId>spock</artifactId>
7 <version>0.0.1-SNAPSHOT</version>
8 <packaging>jar</packaging>
9
10 <name>spock</name>
11 <url>http://maven.apache.org</url>
12
13 <build>
14 <plugins>
15 <!-- Required plugin -->
16 <plugin>
17 <!-- The gmavenplus plugin is used to compile Groovy code -->
18 <groupId>org.codehaus.gmavenplus</groupId>
19 <artifactId>gmavenplus-plugin</artifactId>
20 <version>1.5</version>
21 <executions>
22 <execution>
23 <goals>
24 <goal>compile</goal>
25 <goal>testCompile</goal>
26 </goals>
27 </execution>
28 </executions>
29 </plugin>
30 <!-- Optional plugin-->
31 <plugin>
32 <artifactId>maven-surefire-plugin</artifactId>
33 <version>2.6</version>
34 <configuration>
35 <useFile>false</useFile>
36 <includes>
37 <include>**/*Spec.groovy</include>
38 </includes>
39 </configuration>
40 </plugin>
41 </plugins>
42 </build>
43
44 <dependencies>
45 <!-- Required dependency for using Spock -->
46 <dependency>
47 <groupId>org.spockframework</groupId>
48 <artifactId>spock-core</artifactId>
49 <version>1.0-groovy-2.4</version>
50 <scope>test</scope>
51 </dependency>
52 <!-- Optional dependencies for using Spock -->
53 <dependency> <!-- Specify explicitly the Groovy version that matches the spock version -->
54 <groupId>org.codehaus.groovy</groupId>
55 <artifactId>groovy-all</artifactId>
56 <version>2.4.1</version>
57 </dependency>
58 <!-- Enables mocking of classes (in addition to interfaces) -->
59 <dependency>
60 <groupId>cglib</groupId>
61 <artifactId>cglib-nodep</artifactId>
62 <version>3.1</version>
63 <scope>test</scope>
64 </dependency>
65 <!-- Enables mocking of classes without default constructor (together with CGLIB) -->
66 <dependency>
67 <groupId>org.objenesis</groupId>
68 <artifactId>objenesis</artifactId>
69 <version>2.1</version>
70 <scope>test</scope>
71 </dependency>
72 </dependencies>
73</project>
In the plugins section, we're adding the gmavenplus-plugin
, required to compile groovy code. Optionally, we can also add the maven-surefire-plugin
to generate test reports.
In the dependencies section, we add the required Spock library (and explicitly specify the groovy version to make sure it matches the Spock version). Optionally, we can also add CGLIB
and Objenesis
to enable mocking of classes.
Save the file and if the Maven dependencies are not added automatically to the Eclipse project, right-click the name of the project and choose the option Maven -> Update Project....
To test your environment, create HelloWorld.groovy
(with New -> Other -> Groovy class) in the spock
package inside the src/test/groovy
directory:
1package spock
2
3import spock.lang.Specification
4
5class HelloWorldSpec extends Specification {
6
7 def 'hello world'() {
8 given:
9 def i = 4
10
11 expect:
12 i % 2 == 0
13 }
14}
And run it like a JUnit test with Run As -> Junit Test. The output should be the famous green bar.
Groovy is a language that it's compiled to bytecode that is executed by the JVM, and it's very similar to Java, but with features comparable to Ruby, Python, or Javascript.
For example, everything is treated as an object and semicolons are optional:
1int x = 4 // Valid
2x.getClass() // java.lang.Integer
You can use the def
keyword instead of a type to declare a variable, Groovy will determine the type of the variable at runtime, which gives us some flexibility:
1int x = 4
2def y = 5
3y = "Hello" // Valid
4x = "World" // Error
The def
keyword can also be used to define methods (which makes them public). Methods can accept arguments without types, and the value of the last expression is returned by default (so a return statement is optional):
1def sum(a, b) {
2 a + b
3}
The parentheses are optional when calling a method with at least one argument:
1sum(1, 2) // Valid
2sum 1, 2 // Also valid
And you can use a full string as a method name:
1def "sum of two numbers"(a, b) {
2 a + b
3}
Groovy also supports string interpolation, which allows us to substitute any expression into a string:
1def name = "Carl"
2def hello = "Hello $name"
3println hello // "Hello Carl"
In Java, we have the boolean
(or Boolean
) type to evaluate true/false expressions. But Groovy treats all variables as true
unless the variable is:
null
referenceThis way, in Groovy, statements like these are valid:
1boolean b = "I'm not false" // Evaluates to true
2if(10) { } // Evaluates to true
3if(map) { } // Equivalent to if(map != null && !map.isEmpty() { }
An important consideration is that in Groovy, the ==
operator it's like calling the equals()
method of the object, it doesn't test for identity like in Java:
1def s1 = new String("A String")
2def s2 = new String("A String")
3s1 == s2 // true
Finally, we have closures. A closure is an anonymous block of code (like a function) which can be assigned to variables or passed as an argument to a function and has access to data of the context where it was defined. It can be as simple as:
1def hi = {
2 println "Hi"
3}
4hi();
By default, closures have one parameter named it
:
1def hi = {
2 println it
3}
4hi("Hi");
If you don't pass the parameter, it will be null. Of course, you can define your own parameters (you can omit the type to pass anything):
1def hi = { a, b ->
2 println "$a, $b"
3}
4hi("Hi", "Jerry");
5hi(1, 2);
As you can see, Groovy is not as bulky as Java. Of course, there's a lot more to learn about Groovy, but this should be enough to start coding some tests with Spock. That is what we will do next.
In Spock, you write specifications. Every test class must extend from spock.lang.Specification
:
1import spock.lang.Specification
2
3class HelloWorldTest extends Specification {
4
5}
This class can have many methods, where each of them is considered a scenario (a test case):
1import spock.lang.Specification
2
3class HelloWorldTest extends Specification {
4
5 def "scenario 1"() {
6 }
7
8 def "scenario 2"() {
9 }
10}
In BDD, these scenarios are described in a given-when-then format. Spock supports this style using labels with an optional description. Notice that the assert keyword is optional:
1import spock.lang.Specification
2
3class HelloWorldTest extends Specification {
4
5 def "scenario 1"() {
6 given: "An integer with value 5"
7 def i = 5
8
9 when: "This integer is multiplied by 2"
10 i = i * 2
11
12 then: "The final value is 10"
13 assert i != 5
14 i == 10
15 }
16}
If we change one of the conditions so the test fails:
1import spock.lang.Specification
2
3class HelloWorldTest extends Specification {
4
5 def "scenario 1"() {
6 given: "An integer with value 5"
7 def i = 5
8
9 when: "This integer is multiplied by 2"
10 i = i * 2
11
12 then: "The final value is 10"
13 assert i != 5
14 i == 11
15 }
16}
We can see how descriptive Spock presents the failure trace:
1Condition not satisfied:
2
3i == 11
4| |
5| false
610
7
8at spock.HelloWorldTest.scenario 1(HelloWorldTest.groovy:16)
Blocks divide a method into sections. They start with a label and extend either to the beginning of the next block or the end of the method. We already use the given, when and then, but there're six blocks actually:
setup:
(or its alias, given:
)when:
then:
expect:
cleanup:
where:
In the setup:
or given:
(may people prefer this because it's more readable) block you create the objects and do any setup work for the test. Actually, this block is optional can be implicit, anything between the beginning of the method and the first (different) block represents the setup block:
1 setup:
2 def i = 5
If any, there can be only one setup:
or given:
block.
The when:
and then:
blocks always go together. They represent a cause (called stimulus) and an effect (or response). For that reason, the when:
block can contain any code but then:
can only contain conditions, definitions, or interactions. We can have multiple pairs of when-then blocks:
1when:
2i = i * 2
3
4then:
5i == 10
6
7when:
8i = i * 2
9
10then:
11i == 20
Or multiple then blocks after a when (which are evaluated separately):
1when:
2i = i * 2
3
4then:
5assert i != 5
6
7then:
8i == 10
The expect:
block is the same as then:
, it can only contain conditions or variable definitions, but it doesn't require a then:
block:
1expect:
2def i = 5
3i * 2 == 10
The optional cleanup:
block is used to free resources and is run even if the code before the block has thrown an exception. It's usually the last block in the method, but it may only be followed by a where:
block:
1given:
2def file = new File('/temp/1.txt')
3
4//...
5
6cleanup:
7file.close()
where:
is also an optional block that must be the last one because it lets you define values to perform parameterized tests, also known as data-driven tests. It's commonly used with expect:
:
1def 'data-driven test'() {
2 expect:
3 x + y == z
4
5 where:
6 x | y | z
7 3 | 4 | 7
8 19 | 23 | 42
9 71 | 12 | 83
10}
Spock will run as many tests as rows in the where block. In the example, Spock will execute three tests, and the vars x
, y
, and z
will be initialized to the following values:
x
will be 3
, y
will be 4
, and z
will be 7
.x
will be 19
, y
will be 23
, and z
will be 42
.x
will be 71
, y
will be 12
, and z
will be 83
.The minimum number of columns for this to work is two, so if you only have one variable, you can do something like this:
1where:
2x | _
33 | _
419 | _
571 | _
The data can be formatted horizontally also. This way, the previous tests can be written as:
1def 'data-driven test'() {
2 expect:
3 x + y == z
4
5 where:
6 x << [3, 19, 71]
7 y << [4, 23, 12]
8 z << [7, 42, 83]
9}
A data pipe (represented by the left-shift operator, <<
) connects a variable to a data provider. In the example, the values of each variable are provided by a list, but the data provider can be any iterable object, like a Collection, a String, or other implementations of Iterable
. Also, the data can be fetched from external sources, like a database:
1def 'data-driven test'() {
2 def sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
3
4 //...
5
6 where:
7 [id, name, email] << sql.rows("select id, name, email from table")
8}
Values can be ignored with an underscore (_
):
1def 'data-driven test'() {
2 def sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
3
4 //...
5
6 where:
7 [_, name, email] << sql.rows("select * from table")
8}
Variables can be assigned directly also:
1where:
2x | y
33 | 4
419 | 23
571 | 12
6z = x + y
And of course, you can use all of these methods at the same time:
1where:
2x | _
33 | _
419 | _
571 | _
6y << [4, 23, 12]
7z = x + y
Although it's not considered a block, there's also an and:
label that can be added to any block to describe and individual part of it:
1def "scenario 1"() {
2 given: "An integer with value 5"
3 def i = 5
4
5 and: "A string with value a"
6 def s = "a"
7
8 // ...
9}
In a data-driven test, we can test many data sets. If one or more fail, for example:
1def 'data-driven test'() {
2 expect:
3 x + y == z
4
5 where:
6 x | y | z
7 3 | 4 | 1
8 19 | 23 | 420
9 71 | 12 | 83
10}
Something like this is the result:
But if we want to be more specific about the failing datasets, we can use the spock.lang.Unroll
annotation:
1@Unroll
2def 'data-driven test'() {
3 expect:
4 x + y == z
5
6 where:
7 x | y | z
8 3 | 4 | 1
9 19 | 23 | 420
10 71 | 12 | 83
11}
This way, each dataset will be reported separately:
And there's more. We can have custom names with placeholders that will be replaced with the values of the variables defined inside the where:
block. Either on the @Unroll
annotation:
1@Unroll('#x plus #y should be #z')
2def 'data-driven test'() {
3 expect:
4 x + y == z
5
6 where:
7 x | y | z
8 3 | 4 | 1
9 19 | 23 | 420
10 71 | 12 | 83
11}
Or at the method name directly:
1@Unroll
2def '#x plus #y should be #z'() {
3 expect:
4 x + y == z
5
6 where:
7 x | y | z
8 3 | 4 | 1
9 19 | 23 | 420
10 71 | 12 | 83
11}
The result:
You also can annotate the whole class to avoid annotating each method:
1@Unroll
2class HelloWorldTest extends Specification {
3
4 def '#x plus #y should be #z'() {
5 expect:
6 x + y == z
7
8 where:
9 x | y | z
10 3 | 4 | 1
11 19 | 23 | 420
12 71 | 12 | 83
13 }
14
15 def '#x minus #y should be #result'() {
16 expect:
17 x - y == result
18
19 where:
20 x | y
21 3 | 1
22 50 | 9
23 result = x - y
24 }
25}
Fixture methods execute code before or after tests. There are four of them.
setup()
and cleanup()
execute before and after each test case respectively. For example:
1class SetupCleanupTest extends Specification {
2 def list = {
3 println "creating the object"
4 [2, 4, 6]
5 }() // Execute the closure immediately
6
7 def setup() {
8 println "setup()"
9 }
10
11 def cleanup() {
12 println "cleanup()"
13 }
14
15 def 'size'() {
16 println "size()"
17 expect:
18 list.size == 3
19 }
20
21 def 'only even numbers'() {
22 println "only even numbers()"
23 expect:
24 list.every {it % 2 == 0}
25 }
26}
The output will be:
1creating the object
2setup()
3size()
4cleanup()
5creating the object
6setup()
7only even numbers()
8cleanup()
As you can see, instance objects are created before each test feature, like if they were defined at the beginning of the setup()
method.
The other two fixture methods, setupSpec()
and cleanupSpec()
, are executed once for the whole class, before and after the test methods. For example:
1class SetupSpecCleanupSpecTest extends Specification {
2 def @Shared dummy = {
3 println "creating shared object"
4 }() // Execute the closure immediately
5
6 def @Shared list
7
8 def setupSpec() {
9 println "setupSpec()"
10 list = [2, 4, 6]
11 }
12
13 def cleanupSpec() {
14 println "cleanupSpec()"
15 }
16
17 def 'size'() {
18 println "size()"
19 expect:
20 list.size == 3
21 }
22
23 def 'only even numbers'() {
24 println "only even numbers()"
25 expect:
26 list.every {it % 2 == 0}
27 }
28}
The output will be:
1creating shared object
2setupSpec()
3size()
4only even numbers()
5cleanupSpec()
You can think of setupSpec()
and cleanupSpec()
as static methods, and as such, they can only use static variables or the ones annotated with spock.lang.Shared
, otherwise, a compiler error will be generated.
As a side note, cleanup()
and cleanupSpec()
are executed even when the test failed, or an exception is thrown by the test.
Sometimes we need to test if an exception is thrown. In Spock we do it like this:
1def 'should throw exception'() {
2 when:
3 def i = 4 / 0
4
5 then:
6 thrown(RuntimeException)
7}
Using the thrown(Class<Throwable> type)
method to indicate we expect an exception. We can even save a reference to the exception to access its properties:
1def 'should throw exception'() {
2 when:
3 def i = 4 / 0
4
5 then:
6 def e = thrown(RuntimeException)
7 e.message != null
8}
When the exception is not thrown, an error like the following will be generated:
1Expected exception of type 'java.lang.RuntimeException', but no exception was thrown
2 at org.spockframework.lang.SpecInternals.checkExceptionThrown(SpecInternals.java:79)
3 at org.spockframework.lang.SpecInternals.thrownImpl(SpecInternals.java:66)
4 at spock.ExceptionTest.should throw exception(ExceptionTest.groovy:11)
Certainly, there's a method to indicate that an exception should not be thrown:
1def 'should not throw exception'() {
2 when:
3 def i = 4 / 2
4
5 then:
6 notThrown(RuntimeException)
7}
Other times, you'll want not to run (ignore) some tests- Spock can provide a way to do this through extensions that are activated by annotations called directives. There are four extensions related to ignoring tests:
@Ignore
@IgnoreIf(predicate)
@IgnoreRest
@Requires(predicate)
A method annotated with @Ignore
will not be executed by Spock no matter what:
1@Ignore("This test shouldn't be run right now")
2def "Test 1"() {
3 //...
4}
A method annotated with @IgnoreIf(predicate)
is ignored only if the given predicate evaluates to true. For the predicate part, we can use:
java.lang.System
class with its methods to read system properties, getProperty(String)
and getProperties()
spock.util.environment.OperatingSystem
class through the built-in os
property to access information on the current operating system, such as its name, version, and familyspock.util.environment.Jvm
class through the built-in jvm
property to access information on the current JVM, such as its Java versionFor example:
1@IgnoreIf({ System.getProperty('user.dir') == '/home' })
2def 'should not run if user.dir is /home'() {
3 //...
4}
5
6@IgnoreIf({ sys['user.name'] != 'root' })
7def 'should not run when user is not root'() {
8 //...
9}
10
11@IgnoreIf({ env.HOME != '/home' })
12def 'should not run if user home is /home'() {
13 //...
14}
15
16@IgnoreIf({ os.macOs })
17def 'should not run on Mac'() {
18 //...
19}
20
21@IgnoreIf({ jvm.java7 })
22def 'should not run in Java 7'() {
23 //...
24}
A method annotated with @IgnoreRest
will be executed by Spock, but all the other tests of the class will be ignored. For example:
1class IgnoreTest extends Specification {
2 @IgnoreRest
3 def 'this one will run'() {
4 // ...
5 }
6
7 def "this one won't run"() {
8 // ...
9 }
10
11 def "this one won't run either"() {
12 // ..
13 }
14}
A method annotated with @Requires(predicate)
is the opposite of @IgnoreIf
, it will be executed only if the given predicate evaluates to true. For example:
1@Requires({ jvm.java8 })
2def 'only run in Java 8'() {
3 //...
4}
@Ignore
, @IgnoreIf
, and @Requires
can annotate the whole class.
Suppose we have a DAO (Data Access Object) interface and a service in Java to interact with a database:
1public class User {
2 private String email;
3
4 public void setEmail(String email) {
5 this.email = email;
6 }
7 public String getEmail() {
8 return this.email;
9 }
10}
11
12public interface UserDao {
13 User save(User user);
14 boolean delete(int id);
15 User get(String email);
16}
17
18public class UserService {
19 private UserDao dao;
20
21 public void setDao(UserDao dao) {
22 this.dao = dao;
23 }
24
25 public boolean registerUser(User user) {
26 boolean saved = false;
27 if(dao.get(user.getEmail()) == null) {
28 dao.save(user);
29 saved = true;
30 }
31 return saved;
32 }
33
34 // ...
35}
Sometimes we want to test the interaction between classes, but don't want to set up a connection to a database or create a complex object just for a unit test. One option is to create a dummy implementation to simulate results. Such implementation is called a mock object.
To ensure compatibility with Java, Spock uses JDK dynamic proxies when mocking interfaces and CGLIB proxies when mocking classes to generate these dummy objects at runtime.
Here's a simple example of how to create a mock implementation of Dao
:
1class MockTest extends Specification {
2 def 'Test service with mock DAO'() {
3 given:
4 // Creating a mock implementation
5 def daoMock = Mock(UserDao)
6
7 // In Groovy, this is one way to call a setter
8 def service = new UserService(dao: daoMock)
9
10 // For any string passed as parameter return a dummy user from Mock
11 daoMock.get(_ as String) >> {
12 String email -> new User(email: email)
13 }
14
15 when:
16 def user = new User()
17 user.email = "[email protected]"
18
19 then:
20 service.registerUser(user) == false
21 }
22}
We can see that to create a mock object, we just have to call the Mock()
method and define the behavior of the methods on the mock object (since initially, mock objects have no behavior). In the example, we are using the >>
operator to define the return value of the method. Also, _ as String
means any object of type String
(you can pass any type here) and you can catch the parameter passed inside the closure.
We can also specify the return values of a mock method for more than one call:
1class MockTest extends Specification {
2 def 'Test service with mock DAO'() {
3 given:
4 // Creating a mock implementation
5 def daoMock = Mock(UserDao)
6 // Setting return values
7 daoMock.delete(_) >> true >> false
8 // Or with a list: daoMock.delete(_) >> [true, false]
9
10 expect:
11 daoMock.delete(1) == true
12 daoMock.delete(1) == false
13 daoMock.delete(2) == false
14 }
15}
In the example, we are define that for the first invocation of UserDao.delete(int)
, this method must return true
, and for the second and subsequent invocations, false
.
In the then:
or expect:
blocks, we can even specify how many times a method may be called with a number or a range:
1n * method() // expect n calls
2(n..m) * method() // expect n to m calls
3(_..m) * method() // expect at most m calls
4(n.._) * method() // expect at least n call
5n * object.method(_) // any single argument (including null)
6n * object.method(*_) // any argument list (including the empty argument list)
7n * object.method(!null) // any non-null argument
8n * object.method(_ as Type) // any non-null argument of type Type
9n * object._ // any method of object
10n * _ // match anything
For example:
1class MockTest extends Specification {
2 def 'Test service with mock DAO'() {
3 given:
4 // Creating a mock implementation
5 def daoMock = Mock(UserDao)
6 daoMock.get(_ as String) >> null
7
8 def service = new UserService(dao: daoMock)
9
10 when:
11 def user = new User()
12 user.email = "[email protected]"
13 service.registerUser(user)
14
15 then:
16 1 * daoMock.save(_ as User) // The save method should be called because the user doesn't exists
17 }
18}
This has been an introduction to the Spock framework and BDD. I hope you can see how readable and accessible is to test with the help of Groovy and this framework.
Of course, there is a lot of functionality that we didn't cover, but this should be enough to start programming test for most of your needs. You can learn more at https://github.com/spockframework/spock or with the excellent book Java Testing with Spock.