Spock - Document -02 - Spock Primer

时间:2024-05-01 14:35:27

Spock Primer

Peter Niederwieser, The Spock Framework TeamVersion 1.1

This chapter assumes that you have a basic knowledge of Groovy and unit testing. If you are a Java developer but haven’t heard about Groovy, don’t worry - Groovy will feel very familiar to you! In fact, one of Groovy’s main design goals is to be the scripting language alongside Java. So just follow along and consult the Groovy documentation whenever you feel like it.

The goals of this chapter are to teach you enough Spock to write real-world Spock specifications, and to whet your appetite for more.

To learn more about Groovy, go to http://groovy-lang.org/.

To learn more about unit testing, go to http://en.wikipedia.org/wiki/Unit_testing.

Terminology

Let’s start with a few definitions: Spock lets you write specifications that describe expected features (properties, aspects) exhibited by a system of interest. The system of interest could be anything between a single class and a whole application, and is also called system under specification (SUS). The description of a feature starts from a specific snapshot of the SUS and its collaborators; this snapshot is called the feature’s fixture.

The following sections walk you through all building blocks of which a Spock specification may be composed. A typical specification uses only a subset of them.

Imports

import spock.lang.*

Package spock.lang contains the most important types for writing specifications.

Specification

class MyFirstSpecification extends Specification {
// fields
// fixture methods
// feature methods
// helper methods
}

A specification is represented as a Groovy class that extends from spock.lang.Specification. The name of a specification usually relates to the system or system operation described by the specification. For example, CustomerSpecH264VideoPlayback, and ASpaceshipAttackedFromTwoSides are all reasonable names for a specification.

Class Specification contains a number of useful methods for writing specifications. Furthermore it instructs JUnit to run specification with Sputnik, Spock’s JUnit runner. Thanks to Sputnik, Spock specifications can be run by most modern Java IDEs and build tools.

Fields

def obj = new ClassUnderSpecification()
def coll = new Collaborator()

Instance fields are a good place to store objects belonging to the specification’s fixture. It is good practice to initialize them right at the point of declaration. (Semantically, this is equivalent to initializing them at the very beginning of the setup()method.) Objects stored into instance fields are not shared between feature methods. Instead, every feature method gets its own object. This helps to isolate feature methods from each other, which is often a desirable goal.

@Shared res = new VeryExpensiveResource()

Sometimes you need to share an object between feature methods. For example, the object might be very expensive to create, or you might want your feature methods to interact with each other. To achieve this, declare a @Shared field. Again it’s best to initialize the field right at the point of declaration. (Semantically, this is equivalent to initializing the field at the very beginning of the setupSpec() method.)

static final PI = 3.141592654

Static fields should only be used for constants. Otherwise shared fields are preferable, because their semantics with respect to sharing are more well-defined.

Fixture Methods

def setup() {}          // run before every feature method
def cleanup() {} // run after every feature method
def setupSpec() {} // run before the first feature method
def cleanupSpec() {} // run after the last feature method

Fixture methods are responsible for setting up and cleaning up the environment in which feature methods are run. Usually it’s a good idea to use a fresh fixture for every feature method, which is what the setup() and cleanup()methods are for.

All fixture methods are optional.

Occasionally it makes sense for feature methods to share a fixture, which is achieved by using shared fields together with the setupSpec() and cleanupSpec() methods. Note that setupSpec() and cleanupSpec() may not reference instance fields unless they are annotated with @Shared.

If fixture methods are overridden in a specification subclass then setup() of the superclass will run before setup() of the subclass. cleanup() works in reverse order, that is cleanup() of the subclass will execute before cleanup() of the superclass. setupSpec() and cleanupSpec() behave in the same way. There is no need to explicitly call super.setup() or super.cleanup() as Spock will automatically find and execute fixture methods at all levels in an inheritance heirarchy.

Feature Methods

def "pushing an element on the stack"() {
// blocks go here
}

Feature methods are the heart of a specification. They describe the features (properties, aspects) that you expect to find in the system under specification. By convention, feature methods are named with String literals. Try to choose good names for your feature methods, and feel free to use any characters you like!

Conceptually, a feature method consists of four phases:

  1. Set up the feature’s fixture

  2. Provide a stimulus to the system under specification

  3. Describe the response expected from the system

  4. Clean up the feature’s fixture

Whereas the first and last phases are optional, the stimulus and response phases are always present (except in interacting feature methods), and may occur more than once.

Blocks

Spock has built-in support for implementing each of the conceptual phases of a feature method. To this end, feature methods are structured into so-called blocks. Blocks start with a label, and extend to the beginning of the next block, or the end of the method. There are six kinds of blocks: setupwhenthenexpectcleanup, and where blocks. Any statements between the beginning of the method and the first explicit block belong to an implicit setup block.

A feature method must have at least one explicit (i.e. labelled) block - in fact, the presence of an explicit block is what makes a method a feature method. Blocks divide a method into distinct sections, and cannot be nested.

Spock - Document -02 - Spock Primer

The picture on the right shows how blocks map to the conceptual phases of a feature method. The where block has a special role, which will be revealed shortly. But first, let’s have a closer look at the other blocks.

Setup Blocks

setup:
def stack = new Stack()
def elem = "push me"

The setup block is where you do any setup work for the feature that you are describing. It may not be preceded by other blocks, and may not be repeated. A setup block doesn’t have any special semantics. The setup: label is optional and may be omitted, resulting in an implicit setup block. The given: label is an alias for setup:, and sometimes leads to a more readable feature method description (see Specifications as Documentation).

When and Then Blocks

when:   // stimulus
then: // response

The when and then blocks always occur together. They describe a stimulus and the expected response. Whereas whenblocks may contain arbitrary code, then blocks are restricted to conditionsexception conditionsinteractions, and variable definitions. A feature method may contain multiple pairs of when-then blocks.

Conditions

Conditions describe an expected state, much like JUnit’s assertions. However, conditions are written as plain boolean expressions, eliminating the need for an assertion API. (More precisely, a condition may also produce a non-boolean value, which will then be evaluated according to Groovy truth.) Let’s see some conditions in action:

when:
stack.push(elem) then:
!stack.empty
stack.size() == 1
stack.peek() == elem
TIP
Try to keep the number of conditions per feature method small. One to five conditions is a good guideline. If you have more than that, ask yourself if you are specifying multiple unrelated features at once. If the answer is yes, break up the feature method in several smaller ones. If your conditions only differ in their values, consider using a data table.

What kind of feedback does Spock provide if a condition is violated? Let’s try and change the second condition tostack.size() == 2. Here is what we get:

Condition not satisfied:

stack.size() == 2
| | |
| 1 false
[push me]

As you can see, Spock captures all values produced during the evaluation of a condition, and presents them in an easily digestible form. Nice, isn’t it?

Implicit and explicit conditions

Conditions are an essential ingredient of then blocks and expect blocks. Except for calls to void methods and expressions classified as interactions, all top-level expressions in these blocks are implicitly treated as conditions. To use conditions in other places, you need to designate them with Groovy’s assert keyword:

def setup() {
stack = new Stack()
assert stack.empty
}

If an explicit condition is violated, it will produce the same nice diagnostic message as an implicit condition.

Exception Conditions

Exception conditions are used to describe that a when block should throw an exception. They are defined using thethrown() method, passing along the expected exception type. For example, to describe that popping from an empty stack should throw an EmptyStackException, you could write the following:

when:
stack.pop() then:
thrown(EmptyStackException)
stack.empty

As you can see, exception conditions may be followed by other conditions (and even other blocks). This is particularly useful for specifying the expected content of an exception. To access the exception, first bind it to a variable:

when:
stack.pop() then:
def e = thrown(EmptyStackException)
e.cause == null

Alternatively, you may use a slight variation of the above syntax:

when:
stack.pop() then:
EmptyStackException e = thrown()
e.cause == null

This syntax has two small advantages: First, the exception variable is strongly typed, making it easier for IDEs to offer code completion. Second, the condition reads a bit more like a sentence ("then an EmptyStackException is thrown"). Note that if no exception type is passed to the thrown() method, it is inferred from the variable type on the left-hand side.

Sometimes we need to convey that an exception should not be thrown. For example, let’s try to express that a HashMapshould accept a null key:

def "HashMap accepts null key"() {
setup:
def map = new HashMap()
map.put(null, "elem")
}

This works but doesn’t reveal the intention of the code. Did someone just leave the building before he had finished implementing this method? After all, where are the conditions? Fortunately, we can do better:

def "HashMap accepts null key"() {
setup:
def map = new HashMap() when:
map.put(null, "elem") then:
notThrown(NullPointerException)
}

By using notThrown(), we make it clear that in particular a NullPointerException should not be thrown. (As per the contract of Map.put(), this would be the right thing to do for a map that doesn’t support null keys.) However, the method will also fail if any other exception is thrown.

Interactions

Whereas conditions describe an object’s state, interactions describe how objects communicate with each other. Interactions are devoted a whole chapter, and so we only give a quick example here. Suppose we want to describe the flow of events from a publisher to its subscribers. Here is the code:

def "events are published to all subscribers"() {
def subscriber1 = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
def publisher = new Publisher()
publisher.add(subscriber1)
publisher.add(subscriber2) when:
publisher.fire("event") then:
1 * subscriber1.receive("event")
1 * subscriber2.receive("event")
}

Expect Blocks

An expect block is more limited than a then block in that it may only contain conditions and variable definitions. It is useful in situations where it is more natural to describe stimulus and expected response in a single expression. For example, compare the following two attempts to describe the Math.max() method:

when:
def x = Math.max(1, 2) then:
x == 2
expect:
Math.max(1, 2) == 2

Although both snippets are semantically equivalent, the second one is clearly preferable. As a guideline, use when-thento describe methods with side effects, and expect to describe purely functional methods.

TIP
Leverage Groovy JDK methods like any() and every() to create more expressive and succinct conditions.

Cleanup Blocks

setup:
def file = new File("/some/path")
file.createNewFile() // ... cleanup:
file.delete()

cleanup block may only be followed by a where block, and may not be repeated. Like a cleanup method, it is used to free any resources used by a feature method, and is run even if (a previous part of) the feature method has produced an exception. As a consequence, a cleanup block must be coded defensively; in the worst case, it must gracefully handle the situation where the first statement in a feature method has thrown an exception, and all local variables still have their default values.

TIP
Groovy’s safe dereference operator (foo?.bar()) simplifies writing defensive code.

Object-level specifications usually don’t need a cleanup method, as the only resource they consume is memory, which is automatically reclaimed by the garbage collector. More coarse-grained specifications, however, might use a cleanupblock to clean up the file system, close a database connection, or shut down a network service.

TIP
If a specification is designed in such a way that all its feature methods require the same resources, use acleanup() method; otherwise, prefer cleanup blocks. The same trade-off applies to setup() methods and setup blocks.

Where Blocks

where block always comes last in a method, and may not be repeated. It is used to write data-driven feature methods. To give you an idea how this is done, have a look at the following example:

def "computing the maximum of two numbers"() {
expect:
Math.max(a, b) == c where:
a << [5, 3]
b << [1, 9]
c << [5, 9]
}

This where block effectively creates two "versions" of the feature method: One where a is 5, b is 1, and c is 5, and another one where a is 3, b is 9, and c is 9.

Although it is declared last the where block is evaluated before feature method runs.

The where block will be further explained in the Data Driven Testing chapter.

Helper Methods

Sometimes feature methods grow large and/or contain lots of duplicated code. In such cases it can make sense to introduce one or more helper methods. Two good candidates for helper methods are setup/cleanup logic and complex conditions. Factoring out the former is straightforward, so let’s have a look at conditions:

def "offered PC matches preferred configuration"() {
when:
def pc = shop.buyPc() then:
pc.vendor == "Sunny"
pc.clockRate >= 2333
pc.ram >= 4096
pc.os == "Linux"
}

If you happen to be a computer geek, your preferred PC configuration might be very detailed, or you might want to compare offers from many different shops. Therefore, let’s factor out the conditions:

def "offered PC matches preferred configuration"() {
when:
def pc = shop.buyPc() then:
matchesPreferredConfiguration(pc)
} def matchesPreferredConfiguration(pc) {
pc.vendor == "Sunny"
&& pc.clockRate >= 2333
&& pc.ram >= 4096
&& pc.os == "Linux"
}

The new helper method matchesPreferredConfiguration() consists of a single boolean expression whose result is returned. (The return keyword is optional in Groovy.) This is fine except for the way that an inadequate offer is now presented:

Condition not satisfied:

matchesPreferredConfiguration(pc)
| |
false ...

Not very helpful. Fortunately, we can do better:

void matchesPreferredConfiguration(pc) {
assert pc.vendor == "Sunny"
assert pc.clockRate >= 2333
assert pc.ram >= 4096
assert pc.os == "Linux"
}

When factoring out conditions into a helper method, two points need to be considered: First, implicit conditions must be turned into explicit conditions with the assert keyword. Second, the helper method must have return type void. Otherwise, Spock might interpret the return value as a failing condition, which is not what we want.

As expected, the improved helper method tells us exactly what’s wrong:

Condition not satisfied:

assert pc.clockRate >= 2333
| | |
| 1666 false
...

A final advice: Although code reuse is generally a good thing, don’t take it too far. Be aware that the use of fixture and helper methods can increase the coupling between feature methods. If you reuse too much or the wrong code, you will end up with specifications that are fragile and hard to evolve.

Using with for expectations

As an alternative to the above helper methods, you can use a with(target, closure) method to interact on the object being verified. This is especially useful in then and expect blocks.

def "offered PC matches preferred configuration"() {
when:
def pc = shop.buyPc() then:
with(pc) {
vendor == "Sunny"
clockRate >= 2333
ram >= 406
os == "Linux"
}
}

Unlike when you use helper methods, there is no need for explicit assert statements for proper error reporting.

When verifying mocks, a with statement can also cut out verbose verification statements.

def service = Mock(Service) // has start(), stop(), and doWork() methods
def app = new Application(service) // controls the lifecycle of the service when:
app.run() then:
with(service) {
1 * start()
1 * doWork()
1 * stop()
}

Specifications as Documentation

Well-written specifications are a valuable source of information. Especially for higher-level specifications targeting a wider audience than just developers (architects, domain experts, customers, etc.), it makes sense to provide more information in natural language than just the names of specifications and features. Therefore, Spock provides a way to attach textual descriptions to blocks:

setup: "open a database connection"
// code goes here

Individual parts of a block can be described with and::

setup: "open a database connection"
// code goes here and: "seed the customer table"
// code goes here and: "seed the product table"
// code goes here

An and: label followed by a description can be inserted at any (top-level) position of a feature method, without altering the method’s semantics.

In Behavior Driven Development, customer-facing features (called stories) are described in a given-when-then format. Spock directly supports this style of specification with the given: label:

given: "an empty bank account"
// ... when: "the account is credited $10"
// ... then: "the account's balance is $10"
// ...

As noted before, given: is just an alias for setup:.

Block descriptions are not only present in source code, but are also available to the Spock runtime. Planned usages of block descriptions are enhanced diagnostic messages, and textual reports that are equally understood by all stakeholders.

Extensions

As we have seen, Spock offers lots of functionality for writing specifications. However, there always comes a time when something else is needed. Therefore, Spock provides an interception-based extension mechanism. Extensions are activated by annotations called directives. Currently, Spock ships with the following directives:

@Timeout

Sets a timeout for execution of a feature or fixture method.

@Ignore

Ignores a feature method.

@IgnoreRest

Ignores all feature methods not carrying this annotation. Useful for quickly running just a single method.

@FailsWith

Expects a feature method to complete abruptly. @FailsWith has two use cases: First, to document known bugs that cannot be resolved immediately. Second, to replace exception conditions in certain corner cases where the latter cannot be used (like specifying the behavior of exception conditions). In all other cases, exception conditions are preferable.

To learn how to implement your own directives and extensions, go to the Extensions chapter.

Comparison to JUnit

Although Spock uses a different terminology, many of its concepts and features are inspired from JUnit. Here is a rough comparison:

Spock JUnit

Specification

Test class

setup()

@Before

cleanup()

@After

setupSpec()

@BeforeClass

cleanupSpec()

@AfterClass

Feature

Test

Feature method

Test method

Data-driven feature

Theory

Condition

Assertion

Exception condition

@Test(expected=…​)

Interaction

Mock expectation (e.g. in Mockito)