Slide 1

Slide 1 text

Spock: A Highly Logical Way To Test Howard M. Lewis Ship TWD Consulting [email protected] @hlship © 2013 Howard M. Lewis Ship

Slide 2

Slide 2 text

Why Don't We Test? Hard To Get Started Code Too Monolithic More Code Is More Code Test Code Hard To Maintain Uh, We Used To? Tests Broke, Nobody Fixed, Turned Off My Code's Perfect

Slide 3

Slide 3 text

What if ... ✦Test code was readable? ✦Tests were concise? ✦Test reports were useful? ✦Failures were well described? ✦Mocking was easy? … wouldn't that be most logical?

Slide 4

Slide 4 text

Spock ✦Groovy DSL for Testing ✦Builds on JUnit 4 ✦Goals: Concise, Maintainable ✦Given / When / Then ✦Authors: ✦Peter Niederwieser ✦Luke Daley

Slide 5

Slide 5 text

My Path 1990's Wrote own test framework – in PL/1 5000 - 7000 tests 2001 First JUnit tests (Tapestry template parser) 2003 Started using TestNG, EasyMock 2005 Started using Selenium 1 2006 Dabbled in Groovy 2010 Spock! ~ 0.4 2012 Rewrote ~650 tests TestNG ➠ Spock (tapestry-ioc)

Slide 6

Slide 6 text

Feature Testing

Slide 7

Slide 7 text

Terminology Specification Feature Fixture Feature Collaborator Collaborator System Under Specification

Slide 8

Slide 8 text

Sky.java First Specification SkySpecification.groovy package org.example import spock.lang.* class SkySpecification extends Specification { ... } All Specifications extend from this base class package org.example.sus; public class Sky { public String getColor() { return "blue"; } } spock 0.7- groovy-2.0

Slide 9

Slide 9 text

Feature Methods def "sky is blue"() { setup: def sky = new Sky() expect: sky.color == "blue" }

Slide 10

Slide 10 text

Execution $ gradle clean test :clean :compileJava :compileGroovy :processResources UP-TO-DATE :classes :compileTestJava UP-TO-DATE :compileTestGroovy :processTestResources :testClasses :test BUILD SUCCESSFUL Total time: 12.106 secs ~/workspaces/github/spock-examples $ $ tree build/reports/ build/reports/ └── tests ├── base-style.css ├── css3-pie-1.0beta3.htc ├── index.html ├── org.example.SkySpecification.html ├── org.example.html ├── report.js └── style.css 1 directory, 7 files ~/workspaces/github/spock-examples $

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

sky.color == "green"

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

Feature Method Blocks def "sky is blue"() { setup: def sky = new Sky() expect: sky.color == "blue" } ✦ setup: or given: ✦ expect: ✦ when: ✦ then: ✦ where: ✦ cleanup: ✦ Feature methods must contain at least one block otherwise, not a feature method

Slide 18

Slide 18 text

setup: ✦Initialization of the fixture ✦Always at top of method ✦Can't be repeated ✦Can be given: instead ✦Anything up to first label is implicit setup:

Slide 19

Slide 19 text

expect: ✦Combines stimulus and response ✦Contains only conditions and variable definitions ✦Conditions assert Groovy truth ✦Use with pure functions ✦No side effects, just inputs and result expect: sky.color == "blue" Stimulus Response

Slide 20

Slide 20 text

when: / then: def "clouds are grey"() { def sky = new Sky() when: sky.addStormSystem() then: sky.color == "grey" } ✦Tests code with side effects ✦then: may only contain: ✦conditions ✦exception conditions ✦interactions ✦variable definitions Stimulus Response

Slide 21

Slide 21 text

when: / then: def "clouds are grey"() { def sky = new Sky() when: sky.addStormSystem() then: sky.color == "grey" }

Slide 22

Slide 22 text

Test driven Sky.java package org.example.sus; public class Sky { private String color = "blue"; public String getColor() { return color; } public void addStormSystem() { color = "grey"; } }

Slide 23

Slide 23 text

then: Old Values def "pushing an element on the stack increases its size by one"() { def stack = new Stack() when: stack.push("element") then: stack.size() == old(stack.size()) + 1 } Expression value captured before where: block

Slide 24

Slide 24 text

Extended Assert def "use of extended assert"() { expect: assert 4 == 5, "Big Brother says there are four fingers" }

Slide 25

Slide 25 text

cleanup: setup: def file = new File("/some/path") file.createNewFile() // ... cleanup: file.delete() ✦Cleanup external resources ✦Always invoked, even if previous exceptions

Slide 26

Slide 26 text

where: ✦Parameterizes feature method with data ✦Must be last block ✦Can use | or || as separator def "length of crew member names"() { expect: name.length() == length where: name | length "Spock" | 5 "Kirk" | 4 "Scotty" | 6 }

Slide 27

Slide 27 text

where: One entry for three feature method executions

Slide 28

Slide 28 text

where: using lists def "length of crew member names (using lists)"() { expect: name.length() == length where: name << ["Spock", "Kirk", "Scotty"] length << [5, 4, 6] } Could come from external file or database where: [name, age, gender] = sql.execute("select name, age, sex from ⏎ customer")

Slide 29

Slide 29 text

class CronExpressionSpec extends Specification { def propertyMissing(String name) { Calendar[name] } def "test CRON expressions for validity"() { def cal = Calendar.getInstance(); def exp = new CronExpression(expr) cal.set year, month, day, hour, minute, second expect: exp.isSatisfiedBy(cal.time) == satisfied where: expr | year | month | day | hour | minute | second | satisfied "0 15 10 * * ? 2005" | 2005 | JUNE | 1 | 10 | 15 | 0 | true "0 15 10 * * ? 2005" | 2006 | JUNE | 1 | 10 | 15 | 0 | false "0 15 10 * * ? 2005" | 2005 | JUNE | 1 | 10 | 16 | 0 | false "0 15 10 * * ? 2005" | 2005 | JUNE | 1 | 10 | 14 | 0 | false "0 15 10 L-2 * ? 2010" | 2010 | OCTOBER | 29 | 10 | 15 | 0 | true "0 15 10 L-2 * ? 2010" | 2010 | OCTOBER | 28 | 10 | 15 | 0 | false "0 15 10 L-5W * ? 2010" | 2010 | OCTOBER | 26 | 10 | 15 | 0 | true "0 15 10 L-1 * ? 2010" | 2010 | OCTOBER | 30 | 10 | 15 | 0 | true "0 15 10 L-1W * ? 2010" | 2010 | OCTOBER | 29 | 10 | 15 | 0 | true } } Pretty formatting care of IntelliJ

Slide 30

Slide 30 text

where: derived values def "length of crew member names (with derived values)"() { expect: name.length() == length where: name << ["Spock", "Kirk", "Scotty"] length = name.length() }

Slide 31

Slide 31 text

class TimeServiceSpec extends Specification { def ts = new TimeServiceImpl() static final int second = 1000, minute = 60 * second, hour = 60 * minute, day = 24 * hour, bigTime = (3 * day) + (2 * hour) + (10 * minute) + (5 * second) @Unroll def "formatInterval #delta ms, #approximate should be '#expected'"() { expect: ts.formatInterval(start, end, approximate) == expected where: delta | approximate || expected 0 | false || "now" 999 | false || "now" -999 | false || "now" 1000 | false || "in one second" -3000 | false || "three seconds ago" bigTime | true || "in about three days and two hours" now = System.currentTimeMillis() start = new Date(now) end = new Date(now + delta) } } Computed for each iteration

Slide 32

Slide 32 text

Block labels def "clouds are grey"() { given: "A fresh Sky" def sky = new Sky() when: "A storm system rolls in" sky.addStormSystem() then: "It all goes grey" sky.color == "grey" } ✦Allowed, but not (currently) used ✦and: "block" allowed, does nothing

Slide 33

Slide 33 text

Beyond Feature Methods

Slide 34

Slide 34 text

Fields package org.example import org.example.sus.Sky import spock.lang.Specification class SkySpecification extends Specification { def sky = new Sky() def "sky is blue by default"() { expect: sky.color == "blue" } def "clouds are grey"() { given: "A fresh Sky" when: "A storm system rolls in" sky.addStormSystem() then: "It all goes grey" sky.color == "grey" } } New for each feature method

Slide 35

Slide 35 text

Shared Fields class MySpecification extends Specification { @Shared def resource = new AnExpensiveResource() static final PASSWORD = "sayfriendandenter" … } Created once, shared across all instances Statics should be final and immutable

Slide 36

Slide 36 text

Fixture Methods def setup() { … } def cleanup() { … } ✦Create / initialize instance of Specification ✦Invoke setup() ✦Invoke feature method ✦Invoke cleanup()

Slide 37

Slide 37 text

Fixture Methods ✦Instance created for Specification setup / cleanup ✦May only access @Shared and static fields def setupSpec() { … } def cleanupSpec() { … }

Slide 38

Slide 38 text

Fixture Method Inheritance abstract class AbstractSharedRegistrySpecification extends Specification { static Registry registry def methodMissing(String name, args) { registry."$name"(* args) } def setupSpec() { if (registry == null) { registry = IOCUtilities.buildDefaultRegistry() } } def cleanupSpec() { registry.cleanupThread(); } } class AspectInterceptorBuilderImplSpec extends AbstractSharedRegistrySpecification { @Shared private AspectDecorator decorator def setupSpec() { decorator = getService AspectDecorator } Base class setupSpec() invoked first Sublass setupSpec() invoked after base class methodMissing() ➠ registry.getService()

Slide 39

Slide 39 text

Exception Conditions def ins = new ClassInstantiatorImpl(ContextCatcher, ⏎ ContextCatcher.constructors[0], null) def "may not add a duplicate instance context value"() { given: def ins2 = ins.with(String, "initial value") when: ins2.with(String, "conflicting value") then: IllegalStateException e = thrown() e.message == "An instance context value of type java.lang.String ⏎ has already been added." } Or: def e = thrown(IllegalStateException)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Mocks and Interactions

Slide 42

Slide 42 text

Specification Feature Fixture Feature Collaborator Collaborator SystemUnder Specification

Slide 43

Slide 43 text

Configured Instance Mock Object System Under Specification

Slide 44

Slide 44 text

CustomerDAO Payment Processor PaymentProcessor.groovy package org.example.sus class PaymentProcessor { CustomerDAO customerDAO def applyPayment(long customerId, BigDecimal amount) { Customer customer = customerDAO.getById(customerId) if (customer == null) throw new IllegalArgumentException("No customer #$customerId") customer.accountBalance += amount customerDAO.update(customer) } } CustomerDAO.java package org.example.sus; public interface CustomerDAO { Customer getById(long id); void update(Customer customer); }

Slide 45

Slide 45 text

class ApplyPaymentSpecification extends Specification { CustomerDAO dao = Mock() PaymentProcessor processor def setup() { processor = new PaymentProcessor(customerDAO: dao) } … }| Factory method

Slide 46

Slide 46 text

def "unknown customer id is an exception"() { when: processor.applyPayment(12345, 100) then: 1 * dao.getById(12345) >> null IllegalArgumentException e = thrown() e.message == "No customer #12345" } Define behavior for preceding when: block Defining Mock Behavior

Slide 47

Slide 47 text

def "valid customer id for update"() { when: processor.applyPayment(customer.id, 200) then: 1 * dao.getById(customer.id) >> customer 1 * dao.update(customer) customer.accountBalance == 500 where: customer = new Customer(id: 98765, accountBalance: 300) }

Slide 48

Slide 48 text

1 * dao.getById(12345) >> null Number of invocations: cardinality Argument Constraints Returns a value Target and Method Constraints

Slide 49

Slide 49 text

Cardinality 1 * dao.getById(12345) >> null Cardinality Description (_..n) * target.method(…) Up to n times n * target.method(…) Exactly n times (n.._) * target.method(…) At least n times Omitted Optional interaction, must have return value

Slide 50

Slide 50 text

Argument Constraints 1 * dao.getById(12345) >> null Argument Constraint Description _ (underscore) Any argument *_ Any number of arguments !null Any non-null argument value Argument equals value !value Argument does not equal value _ as Type Non-null, assignable to type { it -> … } Closure: validates, returns true / false

Slide 51

Slide 51 text

class InteractionsSpecification extends Specification { Operation mock = Mock() interface Operation { Object op(input) } def isOdd(input) { input % 2 != 0 } def "closure for parameter constraint"() { when: assert mock.op(3) == 6 assert mock.op(7) == 14 then: (2..7) * mock.op({ isOdd(it) }) >> { 2 * it } } Closures for Argument Contraints Return value computed by closure Helper method it is list of arguments

Slide 52

Slide 52 text

def "wrong number of invocations"() { when: assert mock.op(7) == 14 then: (2..7) * mock.op({ isOdd(it) }) >> { 2 * it } } Too few invocations for: (2..7) * mock.op({ isOdd(it) }) >> { 2 * it } (1 invocation) Unmatched invocations (ordered by similarity): None & at org.spockframework.mock.runtime.InteractionScope.verify…

Slide 53

Slide 53 text

def "parameter does not match closure constraint"() { when: assert mock.op(3) == 6 assert mock.op(4) == null assert mock.op(7) == 14 then: _ * mock.op({ isOdd(it) }) >> { 2 * it } } Mocks are Lenient

Slide 54

Slide 54 text

Less Lenient Mocks def "detecting parameter that doesn't match"() { when: assert mock.op(3) == 6 assert mock.op(4) == null assert mock.op(7) == 14 then: _ * mock.op({ isOdd(it) }) >> { 2 * it } 0 * _ } Too many invocations for: 0 * _ (1 invocation) Matching invocations (ordered by last occurrence): 1 * mock.op(4) <-- this triggered the error & at org.spockframework.mock.runtime.MockInteraction… aka "any interaction"

Slide 55

Slide 55 text

Simplicity Consistency Efficiency Feedback

Slide 56

Slide 56 text

Target Constraints 1 * dao.getById(12345) >> null Target Constraint Description name Match the named mock _ (underscore) Match any mock

Slide 57

Slide 57 text

Method Constraints 1 * dao.getById(12345) >> null Method Constraint Description name Match method with name _ (underscore) Match any method /regexp/ Match any method that matches the expression e.g. bean./set.*/(_)

Slide 58

Slide 58 text

Return Values 1 * dao.getById(12345) >> null Return Value Constraint Description >> value Return the value >> { … } Evaluate the closure, passed list of arguments, return result >>> [ value1, value2, …] Return the values; final value is repeated May be an Iterable type Need new slide about closure closure w/ 1 arg recieves list of args closure w/ N args recieves actual arguments

Slide 59

Slide 59 text

Chained Return Values then: service.getStatus() >>> ["ok", "ok", "fail"] >> { throw new RuntimeException("Status failure."); } The last value sticks

Slide 60

Slide 60 text

Ordered Interactions def "test three amigos"() { when: facade.doSomething() then: 1 * collab1.firstPart() 1 * collab2.secondPart() then: 1 * collab3.thirdPart() } No order checking on firstPart(), secondPart() thirdPart() only allowed after both firstPart(), secondPart()

Slide 61

Slide 61 text

Stubbing def "frobs the gnop"() { checker.isValid(_) >> true when: … } A "global interaction" valid to end of method

Slide 62

Slide 62 text

Extensions

Slide 63

Slide 63 text

@Unroll @Unroll def "Crew member '#name' length is #length"() { expect: name.length() == length where: name | length "Spock" | 5 "Kirk" | 4 "Scotty" | 6 }

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

@Unroll class CanonicalWhereBlockExampleSpecification extends Specification { def "Crew member '#name' length is #length"() { … } def "length of crew member names (using lists)"() { … } def "length of crew member names (with derived values)"() { … } }

Slide 66

Slide 66 text

@Timeout @Timeout(5) def "can access data in under five seconds"() { … } @Timeout(value=100, unit=TimeUnit.MILLISECONDS) def "can update data in under 100 ms"() { … } ✦Feature and fixture methods run on main thread ✦A second thread may interrupt the main thread ✦Can be placed on class to affect all feature methods

Slide 67

Slide 67 text

@Stepwise ✦Feature methods run in declaration order ✦Failed methods cause remainder to be skipped @Stepwise class BuildingBlocksSpecification extends Specification { def "first step()" { … } def "second step"() { … } def "third step"() { … }

Slide 68

Slide 68 text

@AutoCleanup ✦Will invoke close() on field's value ✦Exceptions are reported but are not failures ✦quiet=true ➠ Don't report exceptions ✦value attribute is name of method to invoke def DatabaseSpecification … { @AutoCleanup @Shared Connection connection … }

Slide 69

Slide 69 text

@Ignore / @IgnoreRest ✦Temporarily control which feature methods execute ✦@Ignore ➠ Ignore this method, run others ✦@IgnoreRest ➠ Run this method, ignore others ✦Ignored Methods Reported

Slide 70

Slide 70 text

More Information

Slide 71

Slide 71 text

Other Spock Topics ✦Stubs ✦Spies ✦Groovy Mocks ✦Creating Extensions ✦Asynchronous Testing ✦Hamcrest Assertions

Slide 72

Slide 72 text

https://github.com/spockframework/spock

Slide 73

Slide 73 text

http://docs.spockframework.org

Slide 74

Slide 74 text

https://github.com/hlship/spock-examples

Slide 75

Slide 75 text

http://howardlewisship.com

Slide 76

Slide 76 text

http://www.flickr.com/photos/28687962@N08/4094051518 http://xkcd.com/703/ http://www.flickr.com/photos/41254175@N07/5878491555 http://www.flickr.com/photos/nolifebeforecoffee/124659356/ http://www.flickr.com/photos/oldpatterns/4561597023/ http://www.flickr.com/photos/marcobellucci/3534516458/ http://www.flickr.com/photos/thomashawk/4958821586 Creative Commons

Slide 77

Slide 77 text

Q & A