3. @nklmish
About me
Senior Software Engineer, Consultant and
architect around JVM technology.
Speaker
Blog - http://nklmish.me
Slideshare- http://slideshare.net/nklmish
7. @nklmish
Hello world spec.
Every specification must extend spock sepcification
Your specification name
class PersonSpec extends spock.lang.Specification {
def “should increment adam's annual salary by 10%"() {
given:
Person adam = new Person()
adam.setSalary(50_000)
when:
adam.grantAnnualBonus(“10%”)
then:
adam.annualSalary() == 55_000
}
}
Feature method
Spock blocks
8. @nklmish
Multiple assertions
Multiple assertions (if any of these condition is false then test will fail)
class PersonSpec extends spock.lang.Specification {
def “should increment adam's annual salary by 10%"() {
given:
Person adam = new Person()
adam.setSalary(50_000)
when:
adam.grantAnnualBonus(“10%”)
then:
adam.annualSalary() == 55_000
adam.isAnnualBonusGranted() //assume it returns true
!adam.isMonthlyBonusGranted() //assume it return false
}
}
9. @nklmish
Instance fields e.g.
Instance field
class PersonSpec extends spock.lang.Specification {
Person adam = new Person()
def “should increment adam's annual salary by 10%"() {
given:
adam.setSalary(50_000)
when:
adam.grantAnnualBonus(“10%”)
then:
adam.annualSalary() == 55_000
adam.isAnnualBonusGranted() //assume it returns true
!adam.isMonthlyBonusGranted() //assume it return false
}
}
12. @nklmish
Use @Shared
field that can be shared b/w ALL test methods
class PersonSpec extends spock.lang.Specification {
@Shared Person adam = new Person()
def “should increment adam's annual salary by 10%"() {
given:
adam.setSalary(50_000)
when:
adam.grantAnnualBonus(“10%”)
then:
adam.annualSalary() == 55_000
adam.isAnnualBonusGranted() //assume it returns true
!adam.isMonthlyBonusGranted() //assume it return false
}
}
18. @nklmish
setup/given e.g.
setup block for a given test method
class LoanSpec extends spock.lang.Specification {
@Shared Cache<Integer, Loan> cache = …//
def “should find loan for a valid loan id"() {
setup:
Loan someLoan = new Loan()
cache.put(5, someLoan)
//…..
}
def “should be able to create a new loan"() {
setup:
Loan loan = new Loan()
//…..
}
}
20. @nklmish
cleanup e.g.
cleanup block for a given test method
class LoanSpec extends spock.lang.Specification {
@Shared Cache<Integer, Loan> cache = …//
def “should find loan for a valid loan id"() {
setup:
cache.put(5, someLoan)
cleanup:
cache.remove(5)
//…..
}
}
21. @nklmish
When, then & expect
when -> contains arbitrary code
then -> restricted to boolean conditions
(assertions), exceptions, interactions &
variable definitions.
expect -> restricted to boolean conditions
and variable definitions.
Note : For purely functional methods “expect”,
for side effect methods “when-then”.
22. @nklmish
E.g.
def “should be able to store elements into the cache” () {
when:
cache.put(5, someLoan)
then:
cache.size() == 1
}
def “should be able to count all elements from the cache” () {
expect:
cache.count() == 10
}
def “should not be able to create a loan with negative amount” () {
when:
Loan loan = new Loan(amount:-100)
then:
IllegalArgumentException ex = thrown()
ex.message == ‘Loan can only be create with amount > 0’
}
23. @nklmish
Assertion Helper
methoddef "should increment adam's annual salary by 10%"() {
given:
Person adam = new Person()
and:
adam.setSalary(50_000)
when:
adam.grantBonus(10)
then:
adam.annualSalary() == 55_000.0
adam.gotBonus()
adam.isHappy()
adam.isChiefExecutive()
}
24. @nklmish
Assertion Helper method
def "should increment adam's annual salary by 10%"() {
given:
Person adam = new Person()
and:
adam.setSalary(50_000)
when:
adam.grantBonus(10)
then:
isBonusPaid(adam, 10_00_000)
}
boolean isBonusPaid(Person person, BigDecimal expectedSalary) {
person.annualSalary() == expectedSalary &&
person.gotBonus() &&
person.isHappy() &&
person.isChiefExecutive()
}
Note: We lost descriptive error message :(
25. @nklmish
Assertion Helper method
def "should increment adam's annual salary by 10%"() {
given:
Person adam = new Person()
and:
adam.setSalary(50_000)
when:
adam.grantBonus(10)
then:
isBonusPaid(adam, 10_00_000)
}
void isBonusPaid(Person person,
BigDecimal expectedSalary) {
assert person.annualSalary() == expectedSalary
assert person.gotBonus()
assert person.isHappy()
assert person.isChiefExecutive()
We have our descriptive message!!
27. @nklmish
E.g.
“quiet = true” => don't report exceptions . (default is true)
“value” => method name to invoke on
annotated object (default is ‘close’)
Note : @Autocleanup can also be used on instance fields
class LoanSpec extends spock.lang.Specification {
@AutoCleanup(quiet = true, value = “closeConnection”)
@Shared
private Database db
setupSpec() {
db.populate() //load test data
}
def “should increment adam's annual salary by 10%"() {
setup:
cache.load()
//…..
cleanup:
cache.clear()
}
}
28. @nklmish
Textually rich
blocks
def "should find customer for a valid customer id"() {
given: 'an existing customer'
def customer = new Customer(firstName: "Joe", dateOfBirth: now())
when: 'we request for customer with a valid id'
def response = mockMvc.perform(get("/api/customers/1"))
then: 'we should receive client details'
1 * customerService.find(1) >> Optional.of(customer)
and:
response.status == 200
}
and label can be used at any top-level
30. @nklmish
@Requires
import spock.util.environment.OperatingSystem;
class PersonSpec extends spock.lang.Specification {
@Shared OperatingSystem os = OperatingSystem.newInstance()
@Requires({env.containsKey(“HASH_KEY_TO_AUTHENTICATE”)})
def “should verify authentication token"() {
}
@Requires({os.isWindows() || isLegacyUser() })
def “should be able to authenticate via window’s credentials"() {
}
static boolean isLegacyUser() {//…}
}
Run test ONLY if given condition(s) is/are met
31. @nklmish
Data pipes
class HelloDataTableSpec extends spock.lang.Specification {
def "total characters in name"() {
expect:
name.trim().size() == totalChars
where:
name << ["Joe "," doe"," hello "]
totalChars << [3, 3, 5]
}
}
Data pipes connects data variable to data provider
Data provider : Any iterable object in groovy
(including csv and sql rows)
32. @nklmish
Data tables, syntactic sugar
class HelloDataTableSpec extends spock.lang.Specification
{
def “should count total number of alphabets in
name"() {
expect:
name.trim().size() == totalChars
where:
name || totalChars
"Joe " || 3
" doe" || 3
" hello " || 5
}
}
33. @nklmish
Data tables failure
reporting
class HelloDataTableSpec extends spock.lang.Specification {
def “should count total number of alphabets in name"() {
expect:
name.trim().size() == totalChars
where:
name || totalChars
"Joe " || 3
" doe" || 3
" hello " || 50
}
}
Can we do better ?
34. @nklmish
Yes, using unroll
@Unroll
class HelloDataTableSpec extends spock.lang.Specification {
def “should count total number of alphabets in #name”() {
expect:
name.trim().size() == totalChars
where:
name || totalChars
"Joe " || 3
" doe" || 3
" hello " || 50
}
}
36. @nklmish
@Ignore
@Ignore(“don’t run any test in this specification”)
class HelloDataTableSpec extends spock.lang.Specification
{
def “should count total number of alphabets in
name"() {
expect:
name.trim().size() == totalChars
where:
name || totalChars
"Joe " || 3
" doe" || 3
" hello " || 5
}
}
37. @nklmish
@Ignore
class HelloDataTableSpec extends spock.lang.Specification
{
@Ignore(“don’t run only this specific test”)
def “should count total number of alphabets in
name"() {
expect:
name.trim().size() == totalChars
where:
name || totalChars
"Joe " || 3
" doe" || 3
" hello " || 5
}
}
41. @nklmish
Spock Mocking
Uses:
JDK dynamic proxies for mocking interfaces.
CGLIB proxies for mocking classes
Mock objects are lenient by default (default behaviour
can be overridden via stubbing)
Default returns values are false, 0 or null (except
Object.toString/hashCode/equals)
Note : Mock can be used for both mocking and stubbing
whereas stubs can be used only for stubbing!!!
42. @nklmish
Mocking E.g.
class LoanSpec extends spock.lang.Specification {
LoanManager loanManager = new LoanManager()
ValidatorManager validatorManager = Mock(ValidatorManager)
RepositoryManager repoManager = Mock(RepositoryManager)
//…
}
43. @nklmish
Interaction E.g.
class LoanSpec extends spock.lang.Specification {
ValidatorManager validatorManager = Mock(ValidatorManager)
RepositoryManager repoManager = Mock(RepositoryManager)
LoanManager loanManager = new LoanManager(validatorManager : validatorManager,
repositoryManager: repoManager)
def "should consult with validator manager and repository manager before saving a new loan application"() {
given:
Loan someLoan = new Loan()
when:
loanManager.save(someLoan)
then:
1 * validatorManager.validate(someLoan)
1 * repoManager.save(someLoan)
}
}
cardinality (how many method calls are expected)
target constraint (mock object)
method constraint (method you are interested in)
argument constraint (expected method argument)
44. @nklmish
Invocation order
class LoanSpec extends spock.lang.Specification {
ValidatorManager validatorManager = Mock(ValidatorManager)
RepositoryManager repoManager = Mock(RepositoryManager)
LoanManager loanManager = new LoanManager(validatorManager : validatorManager,
repositoryManager: repoManager)
def "should consult with validator manager and repository manager before saving a new loan application"() {
when:
loanManager.save(someLoan)
then:
1 * validatorManager.validate(someLoan)
then:
1 * repoManager.save(someLoan)
}
}
45. @nklmish
Detecting Mock Obj
class LoanSpec extends spock.lang.Specification {
ValidatorManager validatorManager = Mock(ValidatorManager)
RepositoryManager repoManager = Mock(RepositoryManager)
LoanManager loanManager = new LoanManager(validatorManager : validatorManager,
repositoryManager: repoManager)
def "should consult with validator manager before saving a new loan application"() {
when:
loanManager.save(someLoan)
then:
1 * validatorManager.validate(someLoan)
new MockDetector().isMock(validatorManager)
}
}
You can get more info using MockDetector, like name, type, etc
46. @nklmish
Stub
When you are ONLY interested in
returning some value in respond to
particular method call OR want to
perform side effect.
47. @nklmish
Stubbing
ValidatorManager validatorManager = Stub(ValidatorManager)
def setup() {
validatorManager.validate(_) >> true
}
}
Note: this will always return true whenever
validatorManager.validate() is invoked
48. @nklmish
Stubbing, return list
of values
ValidatorManager validatorManager = Stub(ValidatorManager)
def setup() {
validatorManager.validate >> [true,false,true]
}
}
translates to: return true for first invocation, false
for second and true for all other invocations
49. @nklmish
Stubbing, throw exception
+ chaining responses
ValidatorManager validatorManager = Stub(ValidatorManager)
def setup() {
validatorManager.validate >> [true,false] >> {throw new
RuntimeException()} >> [true]
}
}
translates to: return true for first invocation, false for
second and throw exception for third and return true for
all other invocations