Quick start with the Spock Framework

Just quick note with examples of using the Spock Framework for writing unit tests.

How to setup?

Add new dependency in external-dependencies.xml:

1
2
3
4
5
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.0-groovy-3.0</version>
</dependency>

Simple test template

An example of simple test.

  • The class should extend spock.lang.Specification
  • The testing source should be under annotation @Subject
  • Resources should be under @Collaborator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@UnitTest
class DefaultTeslaOrderServiceTest extends Specification {

@Subject
DefaultTeslaOrderService orderService

@Collaborator
OrderFulfillmentProcessService orderFulfillmentProcessService = Mock()
@Collaborator
ModelService modelService = Mock()

@Test
def "should submit order"() {
given:
def order = Mock(OrderModel)
order.getCode() >> "someCode"

when:
orderService.submitOrder(order)

then:
1 * orderFulfillmentProcessService.startFulfillmentProcessForOrder(order)
0 * modelService.save(order)
}
}

Mocking

You can mock just by Mock() or Mock(Class)

1
2
3
OrderModel order1 = Mock()
def order2 = Mock(OrderModel)

Stubbing methods

You can simple stub methods by >> instruction:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
def "should populate url from source to target"() {
given:
def source = createMedia(someUrl: SOME_URL)
urlService.getAbsoluteUrl(SOME_URL) >> WEB_SITE_URL + SOME_URL

when:
populator.populate(source, target)

then:
target.someUrl == ABSOLUTE_URL
}

Check invocation

You can check the fact of invocation the method using the expression:

1
2
3
4
5
6
1 * modelService.save(order)
| | | |
| | | argument constraint
| | method constraint
| target constraint
cardinality

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
def "should return status aborted, if it was aborted"() {
given:
def orderA = createOrderModel()
def orderB = createOrderModel()
hitachiOrderService.process() >> [orderA, orderB]
def stub = Stub(CronJobModel) {
getRequestAbort() >> false >> true
}
when:
def result = cronJob.perform(stub)

// check invocation:
then: "first order was processed"
1 * updateOrderStatusStrategy.updateOrderStatus(orderA, "on verification")
1 * hitachiProcessDefinitionFactory.getFulfillmentProcess(_ as OrderModel) >> Stub(OrderProcessModel)
1 * businessProcessService.startProcess(_ as OrderProcessModel)
and: "processing was aborted"
result.status == CronJobStatus.ABORTED
result.result == CronJobResult.SUCCESS
}

You can simple check the different numbers of invocations:

1
2
3
4
5
6
1 * modelService.save(order)      // exactly one call
0 * modelService.save(order) // zero calls
(3..7) * modelService.save(order) // between three and seven calls (inclusive)
(1.._) * modelService.save(order) // at least one call
(_..3) * modelService.save(order) // at most three calls
_ * modelService.save(order) // any number of calls, including zero

Call to any mock object

1
2
1 * modelService.save(order)  // a call to 'modelService'
1 * _.save(order) // a call to any mock object

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
def "test feature"() {
given:
def status = OrderStatus.IN_PROGRESS
def order = Mock(OrderModel)
when:
nasaStatusStrategy.setStatus(status, order)

then:
nasaAppointmentStrategy.hasAppointment() >> false
and:
// a call to 'modelService'
1 * modelService.save(order)
// a call to any mock object
0 * _.setStatus(status)
}

Call any method

The same way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1 * modelService._(*_)    // any method on modelService, with any argument list
1 * modelService._ // the same

1 * _._ // any method call on any mock object
1 * _ // the same

1 * modelService.save(order) // an argument that is equal to the object "order"
1 * modelService.save(!order) // an argument that is unequal to the object "order"
1 * modelService.save(_) // any single argument (including null)
1 * modelService.saveAll(*_) // any argument list (including the empty argument list)
1 * modelService.save(!null) // any non-null argument
1 * modelService.save(_ as OrderModel)// any non-null argument that is-a OrderModel
0 * modelService.save(_ as OrderModel)// don't allow save any orders
order.setCode(endsWith("22")) // a String argument ending with "22"

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@UnitTest
class NetflixMovieControllerSpec extends Specification {

@Subject
NetflixMovieController controller

@Collaborator
NetflixRatingValidator netflixRatingValidator = Mock()
@Collaborator
CustomerFacade customerFacade = Mock()

@Test
def "should throw ValidationException if validator found errors"() {
given:
def form = new MovieRatingForm()
def bindingResult = Mock(BindingResult) {
hasErrors() >>> [false, true]
}

when:
controller.updateRating(form, bindingResult)

then:
1 * netflixRatingValidator.validate(*_)
// any method on customerFacade, with any argument list:
0 * customerFacade._(*_)
and:
thrown(ValidationException)
}
}

More options for stubbing

It works the same way:

1
2
3
4
5
6
7
modelService.clone(_ as OrderModel) >> Mock(OrderModel)
// sequence:
modelService.clone(_ as OrderModel) >>> [order1, order2, order3]
// throw exception:
modelService.save(order) >> { throw new ModelSavingException("ups") }
// return any non null value
modelService.clone(_ as OrderModel) >> _

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@UnitTest
class AppleDefaultCartServiceSpec extends Specification {

// some code

@Test
@Unroll
def "[#method] should return something"() {
given:
def cartEntries = createCartEntries(number: 2)
def cart = createCart(entries: cartEntries)
// return any non null value:
cart.getCode() >> _
cartService.getSessionCart() >> cart
and:
// example of sequence:
appleProductAvailabilityService.isVIP(_ as ProductModel) >>> [firstEntryVIP, secondEntryVIP]

expect:
appleDefaultCartService.hasVIPProductsOnly(cart) == allVIP

// other code
}

}

Group asserting

To do this, use with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@UnitTest
class PriceFactoryStrategySpec extends Specification {

@Subject
PriceFactoryStrategy strategy

@Collaborator
SomeService someService = Mock()

@Test
def "should create correct price for holiday card"() {
given:
def card = createCard(amountCurrency: "USD")
and:
someService.isHolidayCard(card) >> true

when:
def result = strategy.findBasePrice(card)

then:
// take a look here:
with(result) {
currencyIso == "USD"
value == 33.49
net == true
}
}

@Test
def "should expire holiday card"() {
given:
def card = createCard(amountCurrency: "USD")

when:
strategy.useCard(card)

then:
// and here:
with(card) {
1 * setExpired(true)
1 * setAmount(0.00)
1 * setOrder(_)
}
}

// some other code
}

Example of using data tables

It allows us to exercise the same test code multiple times, with varying inputs and expected results.
Here the example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@UnitTest
class AppleDefaultCartServiceSpec extends Specification {

@Subject
AppleDefaultCartService appleDefaultCartService = Spy()

@Collaborator
CartService cartService = Mock()
@Collaborator
AppleProductAvailabilityService appleProductAvailabilityService = Mock()
@Collaborator
AppleDefaultCartService appleDefaultCartService = Mock()

@Test
def "[#method] should return something"() {
given:
def cartEntries = createCartEntries(number: 2)
def cart = createCart(entries: cartEntries)
cart.getCode() >> _
cartService.getSessionCart() >> cart
and:
appleProductAvailabilityService.isVIP(_ as ProductModel) >>> [firstEntryVIP, secondEntryVIP]

expect:
appleDefaultCartService.hasVIPProductsOnly(cart) == allVIP

where:
// You can use data tables:
entriesStatus | firstEntryVIP | secondEntryVIP | allVIP
"all entries vip" | true | true | true
"one entry not vip" | true | false | false
"all entries not vip" | false | false | false
}

}

I hope that this will help you quickly start using the Spock Framework in your work.
Thanks.

Follow me in the group in telegram

Share