spock

52
@nklmish Spock Testing made groovy!! Quality is not an act, it is a habit @nklmish

Upload: nklmish

Post on 15-Apr-2017

327 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: Spock

@nklmish

Spock Testing made groovy!!

Quality is not an act, it is a habit

@nklmish

Page 2: Spock

@nklmish

Agenda

What

Basic & advanced features (hopefully enough to get you started)

Hands on labs.

Page 3: Spock

@nklmish

About me

Senior Software Engineer, Consultant and architect around JVM technology.

Speaker

Blog - http://nklmish.me

Slideshare- http://slideshare.net/nklmish

Page 4: Spock

@nklmish

What is Spock?

Page 5: Spock

@nklmish

Spock…Brings best practises(Junit, Jmock, RSpec) under one umbrella.

Testing & specification framework

Based on Groovy

Makes testing fun (readable & compact code)

Enterprise ready

Compatible (IDEs, continuous integration services & build tools)

Groovy goodies

Page 6: Spock

@nklmish

Basic

Page 7: Spock

@nklmish

Hello world spec.

Every specification must extend spock sepcificationYour 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

Page 8: Spock

@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 } }

Page 9: Spock

@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 } }

Page 10: Spock

@nklmish

Instance fields

No sharing b/w feature methods => Every feature method get its own object

Page 11: Spock

@nklmish

What If I want to share an object b/w feature

methods?

Page 12: Spock

@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 } }

Page 13: Spock

@nklmish

Shared fields

Allows to share an object.

Useful when object creation is expensive

Page 14: Spock

@nklmish

But what about static keyword to share an object

?

Page 15: Spock

@nklmish

static keyword

Use it to share constant values for e.g. static final FOO = 123

“@Shared” has well defined sharing semantics over “static”

Page 16: Spock

@nklmish

Spock Blocks

Page 17: Spock

@nklmish

Setup/given

given -> alias for ‘setup’ -> you can put some code that performs setup work

Non repeatable

Page 18: Spock

@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() //….. } }

Page 19: Spock

@nklmish

cleanup

Non repeatable

Runs no matter what( i.e. runs even if feature method has thrown an exception)

Page 20: Spock

@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) //….. } }

Page 21: Spock

@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”.

Page 22: Spock

@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’

}

Page 23: Spock

@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: adam.annualSalary() == 55_000.0 adam.gotBonus()

adam.isHappy() adam.isChiefExecutive()

}

Page 24: Spock

@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: 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 :(

Page 25: Spock

@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: 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!!

Page 26: Spock

@nklmish

setup/cleanup at the beginning/end of life cycle

Page 27: Spock

@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() } }

Page 28: Spock

@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

Page 29: Spock

@nklmish

Other features

Page 30: Spock

@nklmish

@Requiresimport 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

Page 31: Spock

@nklmish

Data pipesclass 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)

Page 32: Spock

@nklmish

Data tables, syntactic sugarclass 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 } }

Page 33: Spock

@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 ?

Page 34: Spock

@nklmish

Yes, using unroll@Unrollclass 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 } }

Page 35: Spock

@nklmish

Ignoring tests

@Ignore

IgnoreRest

@IgnoreIf

Page 36: Spock

@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 } }

Page 37: Spock

@nklmish

@Ignoreclass 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 } }

Page 38: Spock

@nklmish

@IgnoreRest

def “It will be ignored”() {…}

@IgnoreRest def “It will run”() {…}

Page 39: Spock

@nklmish

@IgnoreIf

def “I will run no matter what”() {…}

@IgnoreIf(os.isWindows()) def “I ll not run on windows”() {…}

Page 40: Spock

@nklmish

Mocking & stubbing

Page 41: Spock

@nklmish

Spock MockingUses:

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!!!

Page 42: Spock

@nklmish

Mocking E.g.

class LoanSpec extends spock.lang.Specification { LoanManager loanManager = new LoanManager() ValidatorManager validatorManager = Mock(ValidatorManager) RepositoryManager repoManager = Mock(RepositoryManager)

//… }

Page 43: Spock

@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)

Page 44: Spock

@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) } }

Page 45: Spock

@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

Page 46: Spock

@nklmish

Stub

When you are ONLY interested in returning some value in respond to particular method call OR want to perform side effect.

Page 47: Spock

@nklmish

Stubbing

ValidatorManager validatorManager = Stub(ValidatorManager) def setup() { validatorManager.validate(_) >> true }

}

Note: this will always return true whenever validatorManager.validate() is invoked

Page 48: Spock

@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

Page 49: Spock

@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

Page 50: Spock

@nklmish

Summary

Builtin mocking framework

Descriptive failure message

Readable tests

Groovy goodies

Extensible

Page 51: Spock

@nklmish

Lab Exercises

Page 52: Spock

@nklmish

Thank you !