Test Often and Prosper

A Java developer's guide to Spock

WARNING: We will Mock Spock

Me

Derek Eskens

@snekse

Dev 15+ years, OPI 3+ years

Java, JS, Groovy

Star Wars > Star Trek

'Foodie for Life' is tattooed across my tummy

Object Partners, Inc.

  • JVM, JS, Mobile, Real-time Data, DevOps
  • ~100 Senior Consultants
  • Omaha, Minneapolis
  • Average Tenure Over 5 years
  • Founded 1996

The new frontier

We now have stakeholders asking for testing metrics
and faster deployment of features

Headline
Learn how ___ deploys to production ___ times a day

Testing now takes less code and less time.

Testing will make you a rockstar

Rockstars prosper

Poll Question Time!

Why not just use JUnit?

You could ...

You could even...


class CrewRecruiterServiceSpockMockitoTest
      extends Specification implements CommonCrew {
    //Inject Mockito mocks into your Spock tests 😱
    @InjectMocks CrewRecruiterService crewRecruiterService

    @Mock PayrollService payrollService

    def setup() {
        MockitoAnnotations.initMocks(this);
    }

    void "SelectForBudget can be tested with Mockito mocks"() {
        Mockito.when(payrollService.getSalary(any(CrewMember)))
               .thenReturn(100000L)

        when:
        List crew = crewRecruiterService.selectForBudget(200000L, POOL)

        then: "Use 3rd party fluent assertion library"
        // AKA: crew == [KIRK, SPOCK]
        assertThat(crew).containsExactly(KIRK, SPOCK).inOrder();
    }
}
                    

Why Spock kicks butt...

JUnit Example

@Test
public void selectForBudget() throws Exception {
  // Mock $100k salary for Kirk & Spock
  when(payrollService.getSalary(KIRK)).thenReturn(100000L);
  when(payrollService.getSalary(SPOCK)).thenReturn(100000L);

  // When selecting crew on a $200k budget
  List<CrewMember> crew = crewRecruiterService.selectForBudget(200000L, POOL);

  // Then Kirk and Spock are the only 2 members selected, and returned in that order.
  Assert.assertEquals(new ArrayList<>(asList(KIRK, SPOCK)), crew);

  //create inOrder object passing any mocks that need to be verified in order
  InOrder inOrder = inOrder(payrollService);
  inOrder.verify(payrollService).getSalary(argThat(arg -> arg == CommonCrew.KIRK));
  inOrder.verify(payrollService).getSalary(argThat(arg -> arg == CommonCrew.SPOCK));
  inOrder.verifyNoMoreInteractions();
}
                    
Spock Example

void "SelectForBudget spends budget in crew priority order /
      and avoids unnecessary calls to payroll service"() {

    when:
    def crew = crewRecruiterService.selectForBudget(200000L, POOL)

    then:
    1 * payrollService.getSalary(KIRK) >> 100000  // first add Kirk

    then:
    1 * payrollService.getSalary(SPOCK) >> 100000 // add Spock after Kirk

    then:
    0 * payrollService.getSalary(_)

    and:
    crew == [KIRK, SPOCK]
}
                    

void "SelectForBudget spends budget in crew priority order /
      and avoids unnecessary calls to payroll service"() {

    when: 'We have enough to pay for Kirk and Spock'
    def crew = crewRecruiterService.selectForBudget(200000L, POOL)

    then: 'We only make our expensive external system salary calls in the order we expect'
    1 * payrollService.getSalary(KIRK) >> 100000  // first add Kirk

    then: 'add Spock'
    1 * payrollService.getSalary(SPOCK) >> 100000 // // add Spock after Kirk

    then: 'no more calls to that service since we have depleted our budget'
    0 * payrollService.getSalary(_)

    and: 'our crew consists of just Kirk and Spock'
    crew == [KIRK, SPOCK]
}
                    

My history with groovy

Me looking at old JUnit tests today

Spock Overview

The 3 best things
Easy mocking. Easy verifications.

def "set course test"() {
    Route routeFromEarthToAndoria = new Route([
        "Hang a left at Mars",
        "Turn right at Alpha Centauri A",
        "Gravity assist from Midos V"])

    when:
    Route results = navigationSystem.setCourse("Andoria")

    then:
//  ↓ verify this mock called exactly 1 time w/ these params
    1 * mockCAS.findRoute("Earth", "Andoria") >> routeFromEarthToAndoria
//                                            ↑ (then return) pojo route
    results.directions.first() == "Hang a left at Mars"

}
                    
Parameterized tests

@Unroll("A budget of #salaryCap pays for #expectedCrew")
void "SelectForBudget adds crew in priority order until budget depleted"() {

    when: 'selecting crew at various salary cap levels'
    def crew = crewRecruiterService.selectForBudget(salaryCap, POOL)

    then: 'we get back crew we can afford in priority order'
    crew == expectedCrew

    where:
    salaryCap   || expectedCrew
    0           || []
    100000      || [KIRK]
    299999      || [KIRK, SPOCK]
    MAX_VALUE   || POOL
}
                    
Spock is Groovy

Closure filterCrewByLevel = { List crew, int rank ->
    crew.findResults { it.rank.commandLevel < rank }
}

// ...

1 * mockService.findOfficers(crewMembers) >> {
    filterCrewByLevel(it, 5)
}

                            
Spock is Groovy > JUnit in Groovy
This is Groovy

def KIRK = new CrewMember(firstName: "James", lastName: "Kirk")

Range oneToTen = (1..10) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

def crewNames = ["Kirk", "Spock", "Ensign Redshirt"] // ArrayList

// Transform a collection
List crew = crewNames.collect { String lastName ->
    long yos = getRandomFromRange(oneToTen) //calling outer scope refs
    new CrewMember(lastName: lastName, yearsOfService: yos)
}

// IT, " vs ', interpolation of strings, elvis, null-safe
crew.each { println "${it.firstName?:''} $it.lastName - $it.rank?.salary" }

assert KIRK != SPOCK  // == is .equals, === for references
                            

http://groovy-lang.org/differences.html

http://groovy-lang.org/syntax.html


@Subject Starship starship = new Starship()
@Collaborator PayrollService payrollService = Mock()
@Shared def POOL = ["Kirk", "Spock", "Ensign Redshirt"].asImmutable()
@Collaborator def crewManager = Spy(CrewManager, constructorArgs: [POOL])

def "A very descriptive name for our test"() {
    def currentCrew = ['RebelSpy'] + POOL  /* given: */

    expect:
    currentCrew.size() == 4

    when:
    def result = starship.findIntruders()

    then:
    (1..4) * payrollService.getSalary(_) //default returns falsey
    1 * crewManager.getExpectedCrew(starship) //concrete call
    (1.._) * crewManager.isSpy(starship, _ as CrewMember) >> { ship, cm ->
        return ship.name == 'Enterprise' && cm.lastName.contains('Spy')
    }
    result.lastName == 'RebelSpy'
}
                            
This is Spock

Man that's Groovy

Spock Deep Dive

Basic structure

class MyFirstSpockSpec extends Specification {

  StringUtil stringUtil = new StringUtil()

  // optional setup / teardown 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

  void testContains() {
    expect:
    stringUtil.contains("Spock", "S")
  }
}
                    
Spec fixture setup

class SetupMethodsAreNotNeededSpec extends MyBaseSpecification {
  // Setup methods aren't really needed
  StringUtil stringUtilOne = new StringUtil()
  @Shared NumberUtil numberUtilOne = new NumberUtil()

  /* ----------------  But if you really want them... ----------------*/
  StringUtil stringUtilTwo
  @Shared NumberUtil numberUtilTwo

  def setup() {
    //super.setup()  //calling super for Fixture methods isn't needed
    stringUtilTwo = new StringUtil()
  }

  def setupSpec() {
    numberUtilTwo = new NumberUtil()  //instance fields must be @Shared
  }
}
                    
Feature method structure

void testSpockIsCool() {
  // The setup: or given: labels are optional
  def crew = [KIRK, SPOCK, SCOTTY]

  expect: "the obvious answer from a fanboy"
  SPOCK == fanboy.whoIsTheCoolest(crew)

  when: "Spock isn't an option"
  fanFav = fanboy.whoIsTheCoolest([KIRK, SCOTTY])

  then: "Kung Fu is cool"
  fanFav == KIRK

  and: "for visual separation"
  fanFav.name.contains('James')

  cleanup: "optional clean up"
  SPOCK.mindMeld(fanboy)
}
                    

Method Names

testVulcanHumor() { ... }
"Vulcans find Mad magazine humor confusing"() { ... }
Self documenting code helps prevent uncomfortable conversations with your future self
Assertions

void "expect assertions to be truthy"() {
  expect:
  SPOCK.fullName == "S'chn T'gai Spock"
  SPOCK.isCool() && ! SPOCK.isFunny()
  SPOCK.rank > 0
  SPOCK.salary  // Truthy

  when:
  SPOCK.tellJoke()

  then:
  thrown(IllogicalRequestException)

  when:
  SPOCK.writeTests()

  then:
  notThrown(IllogicalRequestException)
  noExceptionThrown()  // Rarely used
}
                    
Grouping assertions by subject

void "Registering a crew member is done completely"() {
    when:
    def crewMember = ship.getCrewMemberById(1)

    then:
    with(crewMember) {
        firstName == 'James'
        lastName  == 'Kirk'
        rank.title == 'Captain'
    }
}
                    
Assertions in Helpers

void "getCrew returns the expected size of valid crew members"() {
  when:
  def crew = ship.getCrew()

  then:
  crew.size() == 12

  // This would not tell you _which_ crewMember failed the assertion
  crew.every { crewMember.fullName?:'' != '' }

  crew.each { assertIsValidCrewMember(it) }
}

void assertIsValidCrewMember(crewMember) {
    assert crewMember.fullName?:'' != ''
}
                    
Failures

Condition not satisfied:

crew.collect { it.fullName }.join(';') == [KIRK, RED_SHIRT].collect { it.fullName }.join(';')
|    |                       |         |   |     |          |                       |
|    [James Kirk]            James Kirk|   Kirk  Doe        [James Kirk, John Doe]  James Kirk;John Doe
[Kirk]                                 false
                                       9 differences (52% similarity)
                                       James Kirk(---------)
                                       James Kirk(;John Doe)

at com.objectpartners.eskens.spock.services.CrewRecruiterServiceSpockTest.
                        Crew has a red shirt(CrewRecruiterServiceSpockTest.groovy:76)

com.objectpartners.eskens.spock.services.CrewRecruiterServiceSpockTest > Crew has a red shirt FAILED
    org.spockframework.runtime.SpockComparisonFailure at CrewRecruiterServiceSpockTest.groovy:76
1 test completed, 1 failed
:test FAILED
                    

Parameterized tests IN DEPTH

A simple parameterized test example

@Unroll("A budget of #salaryCap pays for #expectedCrew")
void "SelectForBudget adds crew in priority order until budget depleted"() {

    given: 'a flat rate of 100,000 per member'
    payrollService.getSalary(_) >> 100000

    when: 'selecting crew at various salary cap levels'
    def crew = crewRecruiterService.selectForBudget(salaryCap, POOL)

    then: 'we get back crew we can afford in priority order'
    crew == expectedCrew

    where: 'input of salaryCap should produce output of expectedCrew'
    salaryCap    || expectedCrew
    0            || []
    100000       || [KIRK]
    299999       || [KIRK, SPOCK]
    MAX_VALUE    || POOL
}
                    
Single input data tables

@Unroll("Scotty, warp #warpSpeed")
void "Engines don't blow up if we give it a bad command"() {

    when: 'we tell the ship to warp at various level'
    ship.controls.command('warp', warpSpeed)

    then: 'It never explodes'
    noExceptionThrown()

    where:
    warpSpeed   || _
    null        || _
    0           || _
    1           || _
    MAX_VALUE   || _
    'Bajillion' || _
}
                    
Using Spec values

@Shared CrewManager crewMgr = new CrewManagerTestingUtil()
static final ENTERPRISE = "USS Enterprise - NCC-1701"

@Unroll("#crewMember should be assigned to the #expectedShip")
void "Using Shared and statics"() {
    expect:
    // ...

    where: "props defined outside test must be static"
    crewMember              || expectedShip
    crewMgr.get('Spock')    || ENTERPRISE
    crewMgr.get('Kirk')     || ENTERPRISE
    crewMgr.get('Data')     || 'USS Enterprise - NCC-1701-C'
}
                    
Reference Left Cells

given: 'an initial speed'
ship.controls.command('warp', start)
assert getCurrentWarpSpeed() == start

when:
ship.controls.command("${multiplier} our velocity")

then:
getCurrentWarpSpeed() == finalSpeed

where: 'we reference our start value to calculate the final value'
start | multiplier  || finalSpeed
1     | 'Double'    || start * 2
4     | 'Triple'    || start * 3
7     | 'Quadruple' || 12   //Max warp factor is 12

                    
Extract Common Calculations

expect: 'an initial speed'
sendCommand('warp', start)

expect:
sendCommand("${command} our velocity").getCurrentWarpSpeed() == finalSpeed

where: 'we reference our start value to calculate the final value'
start | command     | multiplier
1     | 'Double'    | 2
7     | 'Quadruple' | 4

finalSpeed = [(start * multiplier), 12].max() //Max warp factor is 12

                    
Iterables as params

def "spock can maths"() {
  expect:
  [a,b].sum(0) == c

  where:
  a << [2, 3, 4]
  b << (2..6).step(2)
  c << "4,7,10".split(',')
}
                            

  where:
  [lastName, rank, yos] << excel.get(1, ['B','C','J'])
                            
Closure params!

when:
command.call(start)

then:
getCurrentWarpSpeed() == finalSpeed

where: 'fun with closures'
maxAwareCaptain = { speed ->
    KIRK.say( speed > 12 ? 'Sulu, WHAT ARE YOU DOING! Slow Down!'
                         : 'Steady as she goes, Mr Sulu')
}

start | command                                    || finalSpeed
1     | { ship.command('warp 2') }                 || 2
6     | { KIRK.say('Warp factor one, Mr. Sulu.') } || 1
12    | maxAwareCaptain                            || 12
13    | maxAwareCaptain                            || 12

                    

Mocks

Defining Mocks and Spies

BridgeInterface bridgeInterface = Mock()
def foodReplicator = Mock(FoodReplicator)
Beamer beamer = Mock(Beamer)

PayrollService payrollService = Spy()
def engineControl = Spy(EngineControl)
def crewManager = Spy(CrewManager, constructorArgs: [POOL])

CrewMember SPOCK = h2.getCrewMember( [ lastName: 'Spock' ])
def spockSpy = Spy(SPOCK) //New feature!

                    
Injecting Mocks - Take your pick

Ship ship = new UssEnterprise()
CrewManager crewManager = Mock()

def setup() {
    ship.crewManager = crewManager
}
                    

CrewManager crewManager = Mock()
Ship ship = new UssEnterprise(crewManager: crewManager)
                    


@Subject Ship ship = new UssEnterprise()
@Collaborator CrewManager crewManager = Mock()

// http://bit.ly/spock-subjects-collaborators-extension
                    
Defining Mock Calls


when: 'a crew member asks for cereal'
computer.command('I want some cereal')

then: """
        expect FoodReplicator called once
        with a quantity param of 1
        and cereal type of 'Sugar Smacks'
      """
1 * foodReplicator.make(1, 'Sugar Smacks')  /*
|   |              |    |
|   |              |    |
|   |              |    argument constraints
|   |              method constraint
|   target constraint
cardinality
*/
                            
Cardinality

/** Taken (almost) directly from the docs **/

1 * computer.command("Lights")      // exactly one call
0 * computer.command("Lights")      // zero calls
(1..3) * computer.command("Lights") // between one and three calls (inclusive)
(1.._) * computer.command("Lights") // at least one call
(_..3) * computer.command("Lights") // at most three calls
/*
  ^^  Groovy Ranges at work
*/

_ * computer.command("Lights")      // any number of calls, including zero
                                    // (rarely needed; see 'Strict Mocking')
computer.command("Lights")          // cardinality is optional, same as: _ *
                    
Fancy Target & Method Constraints

_ * _.getStatus() // Match any getStatus() call for all mocks/spies

_ * tribble._ // Match any method call on a tribble

_ * targetingSystem./Fire.*/ // Match all firing requests via regex

_ * _._  // Match any method call on any mock/spy

(..) * .(*_) // Highly illogical

                    
Argument Constraints

/** Taken (almost) directly from the docs **/
1 * computer.command("lights")     // an argument that is equal to the String "lights"
1 * computer.command(!"lights")    // an argument that is unequal to the String "lights"
1 * computer.command()             // the empty argument list (would never match in our example)
1 * computer.command(_)            // any single argument (including null)
1 * computer.command(*_)           // any argument list (including the empty argument list)
1 * computer.command(!null)        // any non-null argument
1 * computer.command(_ as String)  // any non-null argument that is-a String
1 * computer.command({ it.length() > 3 }) // an argument that satisfies the given predicate
                                          // (here: message length is greater than 3)

1 * foodReplicator.make( {it > 0}, !null, _ )  // Multiple arguments

                    
Strict Mocking

Ensure only expected calls are executed


then: 'expect to make a banana and fail if anything else is made'
1 * foodReplicator.make(1, 'Banana')
0 * foodReplicator.make(*_)
                    
Why are we doing this?
Kobayashi Maru

Let's test what happens if Kirk does the thing

when: 'Kirk pushes the big red button'
def response = bigRedButton.push()

then: 'Klingons have fired on use and disabled out weapons'
targetingSystem.fireAllTheWeapons() >> { throw new WeaponsMalfunction('') }

then: 'the response confirms the targeting failed'
response == "I'm sorry, Captain. I'm afraid I can't do that."

when: 'Kirk pushes the big red button'
response = bigRedButton.push()

then: 'but since he reprogrammed the system'
targetingSystem.fireAllTheWeapons() >> new TargetingResponse()

then: 'the response indicates targeting worked'
response == "Targeting confirmed."

                    
Returning fixed values

computer.request('What is 1 + 1') >> "2"
computer.request('What is 1 + 2') >> "3"
computer.request("Do the thing") >> "Okay" >> "No"
computer.request("Tribble count") >>> ["2", "3", "4", "Many"]
                    
Returning dynamic values

foodReplicator.make(*_) >> {args -> "Food: $args[1]; Qty: $args[0]" }
foodReplicator.make(*_) >> {qty, food -> "Making $qty "+ pluralize(qty, food)}

// Complex mocking
foodReplicator.make(*_) >> {int qty, String food ->
    if (food == 'Cereal') {
        return 'Sorry, we are out of sugar smacks'
    }
    return "Making $qty "+ pluralize(qty, food)
}

// Forcing exceptions
foodReplicator.make( {it <= 0} , _ ) >> { throw new InvalidQuantity(it[0]) }

                    
Spies for Hipsters
Selective Mocking

def payrollService = Spy(PayrollService) {
    // Mock the pay for every crew member the same amount
    _ * getSalaryFor(_ as CrewMember) >> 50_000
}

when:
def amountPaid = accountingService.payCrew()

then: """
      We confirm we ask for getTotalCrewPayroll for our crew,
      but let the impl do it's thing. When the impl calls
      getSalaryFor, that's when our stubbing comes in
      """
1 * payrollService.getTotalCrewPayroll(CREW)
amountPaid ==  600_000 // assuming crew of 12

                        
Wrapping method calls

def alienLibrary = Spy(AlienLibrary)

when:
def result = comms.sendBeacon('Hailing Borg Ship')

then: "We call the actual implementation _after_ we mess with it"
alienLibrary.receive(_) >> { msg ->
    def alteredOutgoingMsg = filterNSFW(msg)
    def alienResp = callRealMethodWithArgs(alteredOutgoingMsg)
    return translateResponse(alienResp)
}
result == "Test Often and Prosper"

                        

Advanced Topics

Spock Modules & Extensions

Spring Module

Provides Spring integration so beans can be injected into integration specs

Write your own extension

Or borrow ones like the great subject/collaborator extension

Spring Integration Testing


@SpringBootTest
class CrewMemberIntegrationTest extends Specification {
  @Autowire MockMvc mvc
  @Autowire CrewRepo crewRepo
  def jsonSlurper = new JsonSlurper()

  void 'can fetch crew member by ID'() {
    def rank = new Rank(level: 5, title: 'Grand Admiral')
    def khan = new CrewMember(lastName:'Khan', rank: rank )
    CrewMember khan = crewRepo.saveAndFlush(khan)

    when:
    def data = jsonSlurper.parseText(getJson(mvc, "/crew/${khan.id}"))

    then:
    data.id != null
    data.lastName == 'Khan'
    data.rank.title == 'Grand Admiral'
  }
}
                        
Spring Integration Testing with Spock Mocks ~ Pt 1

@SpringBootTest
class CrewServiceIntegrationTest extends Specification {

  @Autowired CrewService crewService
  @Autowired ExternalSalaryService mockSalaryService

  // test, tests, tests

  /** Override Spring's beans with Spock mocks **/
  @TestConfiguration
  static class Config {
    private DetachedMockFactory factory = new DetachedMockFactory()

    @Bean
    ExternalSalaryService externalSalaryService() {
        factory.Mock(ExternalSalaryService)
    }
  }
}
                        
Spring Integration Testing with Spock Mocks ~ Pt 2

@Autowired CrewService crewService  // class under test
@Autowired ExternalSalaryService mockSalaryService // Mocked Bean

void "crew service delegates to ExternalSalaryService"() {

  when: 'we have 5 crew members in H2'
  def amt = crewService.getTotalCrewCost()

  then: 'use the mocked spring bean in an integration test 😃'
  mockSalaryService.getSalaryFor(_) >> 100000
  amt == 500000

}
                        

Blog post with more information
Coming soon...  @SpringBean & @SpringSpy

Some existing extensions

@AutoCleanup def fio = new FileIOService()

@Timeout(5)
@Use(DiscoMixin)
@Issue('BUG-1701')
void "is Spock cool"() {
  expect: SPOCK.dance() == "Doin' the Hustle"
}
                            
Conditional Testing

@Ignore("Optional description")

@IgnoreRest //Only run tests annotated w/ this

@IgnoreIf({ System.getProperty("os.name").contains("borg") })

@Requires({ os.holodeck })

@PendingFeature
def "Failing tests marked as skipped"() { expect: false }

@PendingFeature  // Failing Test!  No longer pending :-)
def "marks data driven feature where all iterations pass as _failed!_"() {
    expect: test
    where: test << [true, true, true]
}
                    

JUnit rules


@Rule
public Retry retry = new Retry(3);

void "taking kobayashi maru test"() {
  expect:
  kobayashiMaru.takeTest().results == 'passed'
}

void "access network filesystem"() {
  when:
  federationCommand.requestFiles('Kobayashi Maru')

  then:
  noExceptionThrown()
}

                        
GroovyMock, GroovyStub, GroovySpy

Used for mocking...

  • dynamic methods
  • instances created
    outside your control
  • constructors
  • static methods

Problem

3rd party FoodReplicator is creating new instances of ReplicatedFood

You need to mock what ReplicatedFood#tastesLike() returns

We can't mock or spy FoodReplicator

Using GroovyMock

def anyFood = GroovyMock(ReplicatedFood, global: true)

when:
def courses = foodReplicator.request("A 5 course meal")

then: 'call our mocked static method once'
1 * ReplicatedFood.getStaticMenuOptions() >> ['Chicken', 'Sugar Smacks', 'Soup']

and: 'Every food item created is a mock, so override tastesLike'
5 * anyFood.tastesLike() >> ['Soup', 'Chicken']

and: 'We should know what our dinner will taste like'
courses*.tastesLike() == ['Soup', 'Chicken', 'Chicken', 'Chicken', 'Chicken']

                        

Wanna see somethin' cool?

Testing a Testing Framework


def "@PendingFeature marks data driven feature where all iterations pass as failed"() {
  when:
    runner.runWithImports("""
      class Foo extends Specification {
        @PendingFeature
        def bar() {
          expect: test
          where:
            test << [true, true, true]
        }
       }
    """)
  then:
    AssertionError e = thrown(AssertionError)
    e.message == "Feature is marked with @PendingFeature but passes unexpectedly"
}
                    

That's gangster

Questions?

Resources

Meta

meetspock.appspot.com

Thank You

Credits

  • The Internet
  • People a lot smarter than me
  • Leonard Nimoy