7 reasons why bother learning Spock
Transcript of 7 reasons why bother learning Spock
7reasonswhybotherlearningSpock(forJavadevelopers)
Riga,6thSeptember2016
AboutmeAreasofexpertise
AutomaticTesting/TDD(withSpockofcourse:))
SoftwareCraftsmanship/CodeQuality
Concurrency/ParallelComputing/ReactiveSystems
DeploymentAutomation/ContinuousDelivery
FOSSprojectsauthorandcontributor,blogger,trainer
CTOofsmallsoftwarehouse-Codearte
targetedatclientswhocareaboutthequality
TrainerinBottegaITSolutions
WhybotherlearningSpock?
Reason1BDDspecificationbydefault
BDDspecificationbydefaultclassSimpleCalculatorSpecextendsSpecification{
def"shouldsumtwonumbers"(){given:Calculatorcalculator=newSimpleCalculator()when:intresult=calculator.sum(1,2)then:result==3}}
BDDspecificationbydefaultclassSimpleCalculatorSpecextendsSpecification{
def"shouldsumtwonumbers"(){given:Calculatorcalculator=newSimpleCalculator()when:intresult=calculator.sum(1,2)then:result==3}
[given]/when/then(orexpect)arerequiredtocompilecode
notjustcommentsincode
Reason2PowerAssertions
PowerAssertions-basiccasereusedJavaassertkeyword
assert(2+3)*4!=(2*4)+(3*4)
PowerAssertions-basiccasereusedJavaassertkeyword
assert(2+3)*4!=(2*4)+(3*4)
selfexplainingreasonoffailure
Assertionfailed:
assert(2+3)*4!=(2*4)+(3*4)||||||520false82012
PowerAssertions-morecomplexcasenotonlymathematicalexpressions
Stringword="Spock"intbegin=1intend=3assertword.substring(begin,end)==word[begin..end]
PowerAssertions-morecomplexcasenotonlymathematicalexpressions
Stringword="Spock"intbegin=1intend=3assertword.substring(begin,end)==word[begin..end]
alsoformethodreturntypesandarguments
Assertionfailed:
assertword.substring(begin,end)==word[begin..end]||||||||||po13|||13Spock||poc|Spockfalse
PowerAssertions-othercomplexcaseevenforcomplicatedstructuresandexpressions
assertann.name==bob.name&&ann.age==bob.age
PowerAssertions-othercomplexcaseevenforcomplicatedstructuresandexpressions
assertann.name==bob.name&&ann.age==bob.age
detailedevaluationofsubelements
Assertionfailed:
assertann.name==bob.name&&ann.age==bob.age|||||||Ann||Bobfalse||Person(name:Bob,age:7)|falsePerson(name:Ann,age:4)
PowerAssertions-othercomplexcaseevenforcomplicatedstructuresandexpressions
assertann.name==bob.name&&ann.age==bob.age
detailedevaluationofsubelements
Assertionfailed:
assertann.name==bob.name&&ann.age==bob.age|||||||Ann||Bobfalse||Person(name:Bob,age:7)|falsePerson(name:Ann,age:4)
secondpartignoredasmeaningless
PowerAssertionsself-explaining
optionalassertkeywordinthenandexpectcodeblock
unlessplacedinClosureorseparatemethod
backportedtoGroovy
Reason3Firstclasssupport
forparameterizedtests
Parameterizedtestsdef"shouldsumtwointegers"(){given:Calculatorcalculator=newSimpleCalculator()when:intresult=calculator.add(x,y)then:result==expectedResultwhere:x|y||expectedResult1|2||3-2|3||1-1|-2||-3}
Parameterizedtestsdef"shouldsumtwointegers"(){given:Calculatorcalculator=newSimpleCalculator()when:intresult=calculator.add(x,y)then:result==expectedResultwhere:x|y||expectedResult1|2||3-2|3||1-1|-2||-3}
build-insupportwithwherekeyword
doesnotstoponfailureforgiventestcase
syntacticsugarfortable-likedataformatting
Parameterizedtests-evenbetter@Unrolldef"shouldsumtwointegers(#x+#y=#expectedResult)"(){given:Calculatorcalculator=newSimpleCalculator()when:intresult=calculator.add(x,y)then:result==expectedResultwhere:x|y||expectedResult1|2||3-2|3||1-1|-2||-3}
separatetestforeveryinputparametersset-@UnrollvisiblealsoinreportsandIDEinputparameterspresentedintestname(with#)
datapipesanddataprovidersforadvancedusecases
Reason4Built-inmockingframework
SimpleStubbingclassDaoSpecextendsSpecification{
def"shouldstubmethodcall"(){given:Daodao=Stub()dao.getCount()>>1expect:dao.getCount()==1}}
interfaceDao{intgetCount()Itemsave(Itemitem)}
SimpleStubbing-customlogicclassDaoSpecextendsSpecification{
def"shouldthrowexceptionforspecificinputparameters"(){given:Dao<Item>dao=Stub()dao.save(_)>>{Itemitem->thrownewIllegalArgumentException(item.toString())}when:dao.save(newItem())then:thrown(IllegalArgumentException)}}
_foranyargumentvalue
argumentsavailableinsideClosure
MockinteractionsverificationclassDaoSpecextendsSpecification{
def"shouldstubandverify"(){given:ItembaseItem=newItem()and:Dao<Item>dao=Mock()when:dao.delete(baseItem)then:1*dao.delete(_)}}
1*-methodcalledonce
(_)-withanyvalueasthefirstparameter
StubbingandverifyingtogetherclassDaoSpecextendsSpecification{
def"shouldstubandverify"(){given:ItembaseItem=newItem()and:Dao<Item>dao=Mock()when:ItemreturnedItem=dao.save(baseItem)then:1*dao.save(_)>>{Itemitem->item}and:baseItem.is(returnedItem)}}
hastobedefinedinthesamestatementinthensection
StubbingandverifyingtogetherclassDaoSpecextendsSpecification{
def"shouldstubandverify"(){given:ItembaseItem=newItem()and:Dao<Item>dao=Mock()when:ItemreturnedItem=dao.save(baseItem)then:1*dao.save(_)>>{Itemitem->item}and:baseItem.is(returnedItem)}}
hastobedefinedinthesamestatementinthensectiondesignsuggestion:inmostcasesstubbingandinteractionverificationofthesamemockshouldn'tbeneeded
Reason5Exceptiontesting
Capturingthrownexceptiondef"shouldcaptureexception"(){when:throwNPE()then:NullPointerExceptione=thrown()e.message=="testNPE"}
Capturingthrownexceptiondef"shouldcaptureexception"(){when:throwNPE()then:NullPointerExceptione=thrown()e.message=="testNPE"}
thrownexceptioninterceptedandassignedtovariable
forfurtherasserting
testfailedifnotthrown
orhasunexpectedtype
Exceptionsutilityclasstoplaywithcausechain
Reason6Groovymagic
Groovy-whybother?smarter,shorten,morepowerfulJava
Closuretomakefunctionsfirst-classcitizen
Javacode(inmostcases)isalsovalidGroovycode
flatlearningcurve
seamlesslyintegrationwithJavacode
canuseJavalibraries
Groovy-listsandsetscompactsyntaxforlistandsetcreation
List<String>names=['Ann','Bob','Monica','Scholastica']
Set<Integer>luckyNumbers=[4,7,9,7]asSet//4,7,9
Groovy-listsandsetscompactsyntaxforlistandsetcreation
List<String>names=['Ann','Bob','Monica','Scholastica']
Set<Integer>luckyNumbers=[4,7,9,7]asSet//4,7,9
accessing
StringsecondName=names[1]//BobStringlastName=names[-1]//Scholastica
Groovy-listsandsetscompactsyntaxforlistandsetcreation
List<String>names=['Ann','Bob','Monica','Scholastica']
Set<Integer>luckyNumbers=[4,7,9,7]asSet//4,7,9
accessing
StringsecondName=names[1]//BobStringlastName=names[-1]//Scholastica
modification
names[1]='Alex'//Ann,Alex,Monica,Scholasticanames<<'John'//Ann,Alex,Monica,Scholastica,JohnSet<Integer>withoutSeven=luckyNumbers-7//4,9
Groovy-mapscompactsyntaxformapcreation
Map<String,Integer>childrenWithAge=[Ann:5,Bob:7,Monica:9,Scholastica:7]
Groovy-mapscompactsyntaxformapcreation
Map<String,Integer>childrenWithAge=[Ann:5,Bob:7,Monica:9,Scholastica:7]
accessing
childrenWithAge['Ann']//5
Groovy-mapscompactsyntaxformapcreation
Map<String,Integer>childrenWithAge=[Ann:5,Bob:7,Monica:9,Scholastica:7]
accessing
childrenWithAge['Ann']//5
modification
childrenWithAge['Bob']=8//Ann:5,Bob:8,Monica:9,Scholastica:7
Map<String,Integer>withAlice=childrenWithAge+[Alice:3]//Ann:5,Bob:8,Monica:9,Scholastica:7,Alice:3
FunctionalGroovy-Closuresoperationsoncollection
List<String>names=['Ann','Bob','Monica','Scholastica']
names.findAll{Stringname->name.length()>3}//Monica,Scholastica
FunctionalGroovy-Closuresoperationsoncollection
List<String>names=['Ann','Bob','Monica','Scholastica']
names.findAll{Stringname->name.length()>3}.collect{Stringname->//canbechainedname.toUpperCase()}//MONICA,SCHOLASTICA
FunctionalGroovy-Closuresoperationsoncollection
List<String>names=['Ann','Bob','Monica','Scholastica']
names.findAll{Stringname->name.length()>3}.collect{Stringname->//canbechainedname.toUpperCase()}//MONICA,SCHOLASTICA
ittorefersClosureexecutionargument-insimplecases
Set<Integer>luckyNumbers=[4,7,9,7]asSet
luckyNumbers.findAll{it%2==0}//4
FunctionalGroovy-Closuresinlinedfunctionalinterfaces
//productionJavamethodtocallvoidexecuteMultipleTimes(intnumber,RunnablecodeToExecute);
executeMultipleTimes(5,{println"Executed"})
//orsimplierexecuteMultipleTimes(5){println"Executed"}
(G)Stringsvariablereference
voidprintMagicNumber(intnumber){println"Todaymagicnumberis$number.Congrats!"}
(G)Stringsvariablereference
voidprintMagicNumber(intnumber){println"Todaymagicnumberis$number.Congrats!"}
methodexecution
println"Millisecondssincetheepoch:${System.currentTimeMillis()}."
(G)Stringsvariablereference
voidprintMagicNumber(intnumber){println"Todaymagicnumberis$number.Congrats!"}
methodexecution
println"Millisecondssincetheepoch:${System.currentTimeMillis()}."
multi-linestring
StringmailBody="""HelloUser,Welcometoournewsletter.Haveagoodday!"""
(G)Stringsvariablereference
voidprintMagicNumber(intnumber){println"Todaymagicnumberis$number.Congrats!"}
methodexecution
println"Millisecondssincetheepoch:${System.currentTimeMillis()}."
multi-linestring
StringmailBody="""HelloUser,Welcometoournewsletter.Haveagoodday!""".stripIndent()
Reason7Extensibility
Extensibilityverypowerfulextensionsmechanism
dozensofinternalfeaturesimplementedasextensions
providedout-of-box
@AutoCleanup,@IgnoreIf,@RestoreSystemProperties,...
manyextensionsasexternalprojects
abilitytoreuseJUnit's@Ruleand@ClassRule
Summary
WhySpock?consistandreadabletestcode
testsasspecificationbydefault
allGroovymagicavailabletohelp
chancetolearnnewlanguage
embeddedmockingframework
althoughMockitocanbeusedifpreferred(orneeded)
highlyextensible
compatiblewithtoolssupportingJUnit
What'snext?
What'snext?inmostcasesitisworthtogiveSpockatry
What'snext?inmostcasesitisworthtogiveSpockatry
andfallinlovewithitsreadabilityandsimplicity
What'snext?inmostcasesitisworthtogiveSpockatry
andfallinlovewithitsreadabilityandsimplicity
let'smakeasmallexperiment:)
writeallnewtestsinyourteam/projectentirelyinSpock
foraweek
decideifyoulikedit
tip:sampleSpockconfigurationforGradleandMavenavailableonmyblog
Questions?
MarcinZajączkowski
http://blog.solidsoft.info/@SolidSoftBlog
Thankyou!
MarcinZajączkowski
http://blog.solidsoft.info/@SolidSoftBlog