Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Interesting nooks and crannies of Spock you (may) have never seen before

Interesting nooks and crannies of Spock you (may) have never seen before

I bet that Spock needs not to be introduced to anyone here. For many of us it was an entry point to the thrilling world of Groovy. Spock has a flat learning curve - a sensible Java programmer (without prior Groovy experience) can start writing Spock specifications within several minutes. However, when you combine Groovy & Spock magic, a new fascinating meta-world is created. The exploration of its every nook and cranny can take much longer (and it's definitely much more efficient with a guide ;-).

You can expect a set of more advanced (and/or less known) techniques and mechanisms, the use of which can even more simplify and facilitate the testing of our code. You have to be forewarned that we will also visit those dark areas where it is better not to enter, things that do not work or work in a way nobody expects.

Spoiler. It is a Groovy focused version of my warmly welcomed "Smarter testing Java code with Spock Framework" presentation, enriched with additional tricks and traps.

Marcin Zajączkowski

June 02, 2016
Tweet

More Decks by Marcin Zajączkowski

Other Decks in Programming

Transcript

  1. View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. import static FancyFeatureRequiredIntranetConnectionSpecIT.isUrlAvailable
    @Requires({ isUrlAvailable("http://some-host.intranet/") })
    class FancyFeatureRequiredIntranetConnectionSpecIT extends Specification {
    //could be also from trait or other class
    static boolean isUrlAvailable(String url) {
    //...
    }
    def "should do one thing in intranet"() { ... }
    def "should do another thing in intranet"() { ... }
    ...
    }

    View Slide

  17. import static FancyFeatureRequiredIntranetConnectionSpecIT.isUrlAvailable
    @Requires({ isUrlAvailable("http://some-host.intranet/") })
    class FancyFeatureRequiredIntranetConnectionSpecIT extends Specification {
    //could be also from trait or other class
    static boolean isUrlAvailable(String url) {
    //...
    }
    def "should do one thing in intranet"() { ... }
    def "should do another thing in intranet"() { ... }
    ...
    }
    MissingMethodException

    View Slide

  18. import static FancyFeatureRequiredIntranetConnectionSpecIT.isUrlAvailable
    @Requires({ isUrlAvailable("http://some-host.intranet/") })
    class FancyFeatureRequiredIntranetConnectionSpecIT extends Specification {
    @Memoized
    //could be also from trait or other class
    static boolean isUrlAvailable(String url) {
    //...
    }
    def "should do one thing in intranet"() { ... }
    def "should do another thing in intranet"() { ... }
    ...
    }
    @Memoized

    View Slide

  19. @Requires({ System.getProperty("os.arch") == "amd64" }) //old way
    def "should use optimization on 64-bit systems"() { ... }
    @Requires({ System.getenv().containsKey("ENABLE_CRM_INTEGRATION_TESTS") }) //old way
    def "should do complicated things with CRM"() { ... }

    View Slide

  20. @Requires({ System.getProperty("os.arch") == "amd64" }) //old way
    def "should use optimization on 64-bit systems"() { ... }
    @Requires({ System.getenv().containsKey("ENABLE_CRM_INTEGRATION_TESTS") }) //old way
    def "should do complicated things with CRM"() { ... }

    View Slide

  21. spock.util.environment.OperatingSystem
    os.name os.version
    String getName() //Linux
    String getVersion() //8.1
    Family getFamily() //SOLARIS (enum)
    boolean isLinux()
    boolean isWindows()
    boolean isMacOs()
    boolean isSolaris()
    boolean isOther()
    @Requires({ os.linux || os.macOs })
    def "should use fancy console features"() { ... }

    View Slide

  22. spock.util.environment.Jvm
    java.version java.specification.version
    boolean isJava7() //true only if Java 7
    boolean isJava8Compatible() //true if Java 8+
    String getJavaVersion() //e.g. "1.8.0_05"
    String getJavaSpecificationVersion() //e.g. "1.8"
    @IgnoreIf({ !jvm.java8Compatible })
    def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }

    View Slide

  23. spock.util.environment.Jvm
    java.version java.specification.version
    boolean isJava7() //true only if Java 7
    boolean isJava8Compatible() //true if Java 8+
    String getJavaVersion() //e.g. "1.8.0_05"
    String getJavaSpecificationVersion() //e.g. "1.8"
    @IgnoreIf({ !jvm.java8Compatible })
    def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }

    View Slide

  24. spock.util.environment.Jvm
    java.version java.specification.version
    boolean isJava7() //true only if Java 7
    boolean isJava8Compatible() //true if Java 8+
    String getJavaVersion() //e.g. "1.8.0_05"
    String getJavaSpecificationVersion() //e.g. "1.8"
    @IgnoreIf({ !jvm.java8Compatible })
    def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }

    View Slide

  25. //from trait or util class
    private static final PreconditionContext XXX = new PreconditionContext()
    @IgnoreIf({ !XXX.jvm.java8Compatible })
    def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }
    @Requires({ XXX.os.linux || XXX.os.macOs })
    def "should use fancy console features"() { ... }
    PreconditionContext

    View Slide

  26. //from trait or util class
    private static final PreconditionContext XXX = new PreconditionContext()
    @IgnoreIf({ !XXX.jvm.java8Compatible })
    def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }
    @Requires({ XXX.os.linux || XXX.os.macOs })
    def "should use fancy console features"() { ... }
    PreconditionContext

    View Slide

  27. //from trait or util class
    private static final PreconditionContext XXX = new PreconditionContext()
    @IgnoreIf({ !XXX.jvm.java8Compatible })
    def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }
    @Requires({ XXX.os.linux || XXX.os.macOs })
    def "should use fancy console features"() { ... }
    PreconditionContext

    View Slide

  28. View Slide

  29. setup/cleanup

    View Slide

  30. setup/cleanup
    def "complex logic should work with fixed thread pool"() {
    setup: //alias for 'given:'
    ExecutorService threadPool = Executors.newFixedThreadPool(1)
    when:
    String returnedValue = threadPool
    .submit({ "val" } as Callable)
    .get(1, TimeUnit.SECONDS)
    then:
    returnedValue == "val"
    cleanup:
    threadPool?.shutdown()
    }
    where

    View Slide

  31. setup/cleanup
    def "complex logic should work with fixed thread pool"() {
    setup:
    ExecutorService threadPool = Executors.newFixedThreadPool(1)
    when:
    String returnedValue = threadPool
    .submit({ "val" } as Callable)
    .get(1, TimeUnit.SECONDS)
    then:
    returnedValue == "val"
    cleanup:
    threadPool?.shutdown()
    }

    View Slide

  32. def "complex logic should work with fixed thread pool"() {
    setup:
    ExecutorService threadPool = Executors.newFixedThreadPool(1)
    and:
    Callable task = { "val" } as Callable
    when:
    String returnedValue = threadPool
    .submit(task)
    .get(1, TimeUnit.SECONDS)
    then:
    returnedValue == "val"
    cleanup:
    threadPool?.shutdown()
    }

    View Slide

  33. def "complex logic should work with fixed thread pool"() {
    setup:
    ExecutorService threadPool = Executors.newFixedThreadPool(1)
    and:
    Callable task = { "val" } as Callable
    when:
    String returnedValue = threadPool
    .submit(task)
    .get(1, TimeUnit.SECONDS) //null is returned!
    then:
    returnedValue == "val"
    cleanup:
    threadPool?.shutdown()
    }
    Runnable Callable

    View Slide

  34. def "complex logic should work with fixed thread pool"() {
    setup:
    ExecutorService threadPool = Executors.newFixedThreadPool(1)
    and:
    Callable task = [call: { "val" }] as Callable
    when:
    String returnedValue = threadPool
    .submit(task)
    .get(1, TimeUnit.SECONDS)
    then:
    returnedValue == "val"
    cleanup:
    threadPool?.shutdown()
    }

    View Slide

  35. class FancyDBSpec extends Specification {
    @Shared
    private DBConnection dbConnection
    def beforeSpec() {
    dbConnection = new H2DBConnection(...)
    }
    def cleanupSpec() {
    dbConnection.close()
    }
    def "should do fancy things on DB"() { ... }
    def "should do fancy2 things on DB"() { ... }
    }
    interface DBConnection {
    ...
    void close()
    }

    View Slide

  36. class FancyDBSpec extends Specification {
    @Shared
    private DBConnection dbConnection = new H2DBConnection(...)
    def cleanupSpec() {
    dbConnection.close()
    }
    def "should do fancy things on DB"() { ... }
    def "should do fancy2 things on DB"() { ... }
    }
    interface DBConnection {
    ...
    void close();
    }

    View Slide

  37. class FancyDBSpec extends Specification {
    @Shared
    private DBConnection dbConnection = new H2DBConnection(...)
    def cleanupSpec() {
    dbConnection.close()
    }
    def "should do fancy things on DB"() { ... }
    def "should do fancy2 things on DB"() { ... }
    }
    interface DBConnection {
    ...
    void close();
    }

    View Slide

  38. class FancyDBSpec extends Specification {
    @Shared
    @AutoCleanup
    private DBConnection dbConnection = new H2DBConnection(...)
    def "should do fancy things on DB"() { ... }
    def "should do fancy2 things on DB"() { ... }
    }
    interface DBConnection {
    ...
    void close();
    }
    close()

    View Slide

  39. class FancyDBSpec extends Specification {
    @Shared
    @AutoCleanup("release")
    private DBConnection dbConnection = new H2DBConnection(...)
    def "should do fancy things on DB"() { ... }
    def "should do fancy2 things on DB"() { ... }
    }
    interface DBConnection {
    ...
    void release();
    }

    View Slide

  40. cleaup cleanupSpec
    close()
    @AutoCleanup("release")
    @AutoCleanup(quiet = true)

    View Slide

  41. @Before @After
    setupSpec() setup() cleanup() cleanupSpec()
    @BeforeClass @Before @After @AfterClass

    View Slide

  42. +---------------+
    | Specification |
    +---------------+

    View Slide

  43. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+
    | BaseIntegrationSpec |
    +---------------------+

    View Slide

  44. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+
    | BaseIntegrationSpec |
    +---------------------+
    ^
    |
    +------------------+
    | BaseDatabaseSpec |
    +------------------+

    View Slide

  45. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+
    | BaseIntegrationSpec |
    +---------------------+
    ^
    |
    +------------------+
    | BaseDatabaseSpec |
    +------------------+
    ^ ^
    | |
    | |
    +-------- + +---------+
    | DbSpec1 | | DbSpec2 |
    +---------+ +---------+

    View Slide

  46. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+
    | BaseIntegrationSpec |
    +---------------------+
    ^ ^
    | |
    +------------------+ +----------------- +
    | BaseDatabaseSpec | | BaseWiremockSpec |
    +------------------+ +------------------+
    ^ ^ ^ ^
    | | | |
    | | | |
    +-------- + +---------+ +---------------+ +---------------+
    | DbSpec1 | | DbSpec2 | | WiremockSpec1 | | WiremockSpec2 |
    +---------+ +---------+ +---------------+ +---------------+

    View Slide

  47. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+ +--------------------------- +
    | BaseIntegrationSpec |<-------| BaseWiremockWithDbSpec ??? |
    +---------------------+ +----------------------------+
    ^ ^
    | |
    +------------------+ +----------------- +
    | BaseDatabaseSpec | | BaseWiremockSpec |
    +------------------+ +------------------+
    ^ ^ ^ ^
    | | | |
    | | | |
    +-------- + +---------+ +---------------+ +---------------+
    | DbSpec1 | | DbSpec2 | | WiremockSpec1 | | WiremockSpec2 |
    +---------+ +---------+ +---------------+ +---------------+

    View Slide

  48. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+ +--------------------------- +
    | BaseIntegrationSpec |<-------| BaseWiremockWithDbSpec ??? |
    +---------------------+ +----------------------------------+
    ^ ^ | BaseWiremockWithActiveMqSpec ??? |
    | | +----------------------------------+
    +------------------+ +----------------- +
    | BaseDatabaseSpec | | BaseWiremockSpec |
    +------------------+ +------------------+
    ^ ^ ^ ^
    | | | |
    | | | |
    +-------- + +---------+ +---------------+ +---------------+
    | DbSpec1 | | DbSpec2 | | WiremockSpec1 | | WiremockSpec2 |
    +---------+ +---------+ +---------------+ +---------------+

    View Slide

  49. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+ +--------------------------- +
    | BaseIntegrationSpec |<-------| BaseWiremockWithDbSpec ??? |
    +---------------------+ +----------------------------------+
    ^ ^ | BaseWiremockWithActiveMqSpec ??? |
    | | +----------------------------------+
    +------------------+ +----------------- +
    | BaseDatabaseSpec | | BaseWiremockSpec |
    +------------------+ +------------------+
    ^ ^ ^ ^
    | | | |
    | | | |
    +-------- + +---------+ +---------------+ +---------------+
    | DbSpec1 | | DbSpec2 | | WiremockSpec1 | | WiremockSpec2 |
    +---------+ +---------+ +---------------+ +---------------+

    View Slide

  50. View Slide

  51. trait DatabaseTrait {
    static DBConnection dbConnection
    def setupSpec() { //or @BeforeClass or inline
    //prepare DB
    }
    def cleanupSpec() { //or @AfterClass
    //clean up DB
    }
    }

    View Slide

  52. trait DatabaseTrait {
    static DBConnection dbConnection
    def setupSpec() { //or @BeforeClass or inline
    //prepare DB
    }
    def cleanupSpec() { //or @AfterClass
    //clean up DB
    }
    }
    //no more infrastructure code needed in specifications
    class Fancy1DatabaseSpec extends Specification implements DatabaseTrait {
    def "should do fancy1 thing on database"() {
    //fancy1 things on database
    }
    }
    class Fancy2DatabaseSpec extends Specification implements DatabaseTrait {
    def "should do fancy2 thing on database"() {
    //fancy2 things on database
    }
    }

    View Slide

  53. trait DatabaseTrait {
    ...
    }
    trait ActiveMQTrait {
    ...
    }
    trait WireMockTrait {
    ...
    }
    class VerySpecificIntegrationSpec extends Specification
    implements DatabaseTrait, WireMockTrait {
    def "should do very specific integration things"() { ... }
    }

    View Slide

  54. @AutoCleanup cleanup(Spec)
    @Shared static
    static

    View Slide

  55. View Slide

  56. @Unroll
    def "should validate PESEL (#pesel) correctness (#isValid)"() {
    expect:
    sut.validate(pesel) == isValid
    where:
    pesel || isValid
    "123" || false
    "abcd" || false
    "97110208631" || true
    }
    where

    View Slide

  57. @Unroll
    def "should validate PESEL (#pesel) correctness (#isValid)"() {
    expect:
    sut.validate(pesel) == isValid
    where:
    pesel || isValid
    "123" || false
    "abcd" || false
    "97110208631" || true
    }
    #pesel #isValid

    View Slide

  58. @Unroll("PESEL '#pesel' should be #description")
    def "should validate PESEL correctness"() {
    expect:
    sut.validate(pesel) == isValid
    where:
    pesel || isValid
    "123" || false
    "abcd" || false
    "97110208631" || true
    description = isValid ? "valid" : "invalid"
    }

    View Slide

  59. @Unroll("PESEL '#pesel' should be #description")
    def "should validate PESEL correctness"() {
    expect:
    sut.validate(pesel) == isValid
    where:
    pesel || isValid
    "123" || false
    "abcd" || false
    "97110208631" || true
    description = isValid ? "valid" : "invalid"
    }

    View Slide

  60. @Unroll("PESEL '#pesel' should be #description")
    def "should validate PESEL correctness"() {
    expect:
    sut.validate(pesel) == isValid
    where:
    pesel << ["123", "abcd", "97110208631"]
    isValid << [false, false, true]
    description = isValid ? "valid" : "invalid"
    }
    <<

    View Slide

  61. @Unroll("#pesel is valid (#dbId)")
    def "should validate PESEL correctness (CSV)"() {
    expect:
    sut.validate(pesel)
    where:
    [dbId, _, _, pesel] << readValidPeopleFromCSVFile()
    .readLines().collect { it.split(',') }
    //ugly way to read CSV - don't do this
    }

    View Slide

  62. @Unroll

    View Slide

  63. @Unroll
    @Unroll
    @Unroll
    class PeselValidatorSpec extends Specification {
    def "should pass valid PESEL #number"() { ... }
    def "should reject too short PESEL #number"() { ... }
    def "should pass Mr. President PESEL"() { ... }
    (...)
    }

    View Slide

  64. //dependency in Gradle/Maven/Ant
    testCompile 'info.solidsoft.spock:spock-global-unroll:0.5.1'
    @Unroll
    class PeselValidatorSpec extends Specification {
    //unrolled automatically without @Unroll
    def "should pass valid PESEL #number"() { ... }
    (...)
    }

    View Slide

  65. @Roll @DisableGlobalUnroll
    @Roll
    class PeselValidatorSpec extends Specification {
    //one big test for multiple input parameters
    def "should not be unrolled for some reasons PESEL #number"() { ... }
    (...)
    }

    View Slide

  66. @Unroll @Roll
    @Roll
    class PeselValidatorSpec extends Specification {
    //one big test for multiple input parameters
    def "should not be unrolled for some reasons PESEL #number"() { ... }
    //unrolled anyway
    @Unroll("PESEL '#pesel' should be #description")
    def "should validate PESEL correctness"() { ... }
    (...)
    }

    View Slide

  67. View Slide

  68. class DaoSpec extends Specification {
    def "should stub consecutive calls" () {
    given:
    Dao dao = Stub(Dao)
    dao.getCount() >> 1 >> 2
    expect:
    dao.getCount() == 1
    and:
    dao.getCount() == 2
    and:
    dao.getCount() == 2
    }
    }

    View Slide

  69. class DaoSpec extends Specification {
    def "should stub consecutive calls" () {
    given:
    Dao dao = Stub(Dao)
    dao.getCount() >>> [1, 2]
    expect:
    dao.getCount() == 1
    and:
    dao.getCount() == 2
    and:
    dao.getCount() == 2
    }
    }

    View Slide

  70. class DaoSpec extends Specification {
    def "should stub consecutive calls (exception)"() {
    given:
    Dao dao = Stub(Dao)
    dao.getCount() >> 1 >> { throw new IllegalArgumentException() } >> 3
    expect:
    dao.getCount() == 1
    when:
    dao.getCount()
    then:
    thrown(IllegalArgumentException)
    expect:
    dao.getCount() == 3
    }

    View Slide

  71. class DaoSpec extends Specification {
    def "should stub consecutive calls (exception)"() {
    given:
    Dao dao = Stub(Dao)
    dao.getCount() >> 1 >> { throw new IllegalArgumentException() } >> 3
    expect:
    dao.getCount() == 1
    when:
    dao.getCount()
    then:
    thrown(IllegalArgumentException)
    expect:
    dao.getCount() == 3
    }

    View Slide

  72. class DaoSpec extends Specification {
    def "should stub consecutive calls (exception)"() {
    given:
    Dao dao = Stub(Dao)
    dao.getCount() >> 1 >> { throw new IllegalArgumentException(???) } >> 3
    expect:
    dao.getCount() == 1
    when:
    dao.getCount()
    then:
    thrown(IllegalArgumentException)
    expect:
    dao.getCount() == 3
    }

    View Slide

  73. class Dao {
    T save(T element)
    }
    class DaoSpec extends Specification {
    def "should stub method call to return input value"() {
    given:
    Item baseItem = new Item()
    and:
    Dao dao = Stub()
    dao.save(_) >> { it[0] }
    when:
    Item returnedItem = dao.save(baseItem)
    then:
    baseItem.is(returnedItem)
    }
    }

    View Slide

  74. class DaoSpec extends Specification {
    def "should demonstrate common error with one parameter"() {
    given:
    Item baseItem = new Item()
    and:
    Dao dao = Stub()
    dao.save(_) >> { it } //broken - 'it' is an array - it[0]
    when:
    Item returnedItem = dao.save(baseItem)
    then:
    baseItem.is(returnedItem)
    }
    }

    View Slide

  75. class DaoSpec extends Specification {
    def "should stub method call to return input value"() {
    given:
    Item baseItem = new Item()
    and:
    Dao dao = Stub()
    dao.save(_) >> { Item item -> item }
    when:
    Item returnedItem = dao.save(baseItem)
    then:
    baseItem.is(returnedItem)
    }
    }

    View Slide

  76. class DaoSpec extends Specification {
    def "should stub and verify"() {
    given:
    Item baseItem = new Item()
    and:
    Dao dao = Mock()
    dao.save(_) >> { Item item -> item }
    when:
    Item returnedItem = dao.save(baseItem)
    then:
    1 * dao.save(_)
    and:
    baseItem.is(returnedItem)
    }
    }

    View Slide

  77. class DaoSpec extends Specification {
    def "should stub and verify"() {
    given:
    Item baseItem = new Item()
    and:
    Dao dao = Mock()
    dao.save(_) >> { Item item -> item }
    when:
    Item returnedItem = dao.save(baseItem)
    then:
    1 * dao.save(_)
    and:
    baseItem.is(returnedItem)
    }
    }

    View Slide

  78. class DaoSpec extends Specification {
    def "should stub and verify"() {
    given:
    Item baseItem = new Item()
    and:
    Dao dao = Mock()
    when:
    Item returnedItem = dao.save(baseItem)
    then:
    1 * dao.save(_) >> { Item item -> item }
    and:
    baseItem.is(returnedItem)
    }
    }
    then

    View Slide

  79. @Ignore("broken")
    def "notification should be send after business operation (ignore order)"() {
    given:
    Dao dao = Mock()
    Notifier notifier = Mock()
    and:
    def sut = new BusinessService(dao, notifier)
    when:
    sut.processOrder()
    then:
    1 * dao.saveOrder(_) //order does not matter
    1 * notifier.sendNotification(_)
    }

    View Slide

  80. def "notification should be send after business operation"() {
    given:
    Dao dao = Mock()
    Notifier notifier = Mock()
    and:
    def sut = new BusinessService(dao, notifier)
    when:
    sut.processOrder()
    then:
    1 * dao.saveOrder(_)
    then:
    1 * notifier.sendNotification(_)
    }

    View Slide

  81. @Ignore("broken")
    def "notification should be send after business operation (with 'and')"() {
    given:
    Dao dao = Mock()
    Notifier notifier = Mock()
    and:
    def sut = new BusinessService(dao, notifier)
    when:
    sut.processOrder()
    then:
    1 * dao.saveOrder(_)
    and: //'and' does not verify execution order
    1 * notifier.sendNotification(_)
    }
    and

    View Slide

  82. View Slide

  83. 1 * tacticalStation.fireTorpedo(2) //equal to 2
    1 * tacticalStation.fireTorpedo(!2) //not equal to 2
    1 * tacticalStation.fireTorpedo(_) //any argument
    1 * scienceStation.analyzeInput(!null) //not null argument
    1 * scienceStation.analyzeInput(_ as String) //instance of String
    1 * tacticalStation.fireTorpedo({ it > 2 }) //any argument greater than 2
    1 * ts.smellyFindNumberOfShipsInRangeByCriteria(_, { it.contains("fiant") }, 4)

    View Slide

  84. 1 * tacticalStation.fireTorpedo(2) //equal to 2
    1 * tacticalStation.fireTorpedo(!2) //not equal to 2
    1 * tacticalStation.fireTorpedo(_) //any argument
    1 * scienceStation.analyzeInput(!null) //not null argument
    1 * scienceStation.analyzeInput(_ as String) //instance of String
    1 * tacticalStation.fireTorpedo({ it > 2 }) //any argument greater than 2
    1 * ts.smellyFindNumberOfShipsInRangeByCriteria(_, { it.contains("fiant") }, 4)
    ts.smellyFindNumberOfShipsInRangeByCriteria(_, { it.contains("fiant") }, 4) >> 2

    View Slide

  85. View Slide

  86. def dao = Mock(Dao) //Groovy way

    View Slide

  87. def dao = Mock(Dao) //Groovy way
    Dao dao = Mock(Dao) //Java way

    View Slide

  88. def dao = Mock(Dao) //Groovy way
    Dao dao = Mock(Dao) //Java way
    Dao dao = Mock() //shorter Java way?

    View Slide

  89. def dao = Mock(Dao) //Groovy way
    Dao dao = Mock(Dao) //Java way
    Dao dao = Mock() //shorter Java way

    View Slide

  90. def dao = Mock(Dao) //Groovy way
    Dao dao = Mock(Dao) //Java way
    Dao dao = Mock() //shorter Java way

    View Slide

  91. View Slide

  92. View Slide

  93. View Slide

  94. interface Dao {
    Optional getMaybeById(long id)
    ...
    }
    def "should not fail on unstubbed call with Optional return type"() {
    given:
    Dao dao = Stub()
    when:
    dao.getMaybeById(5)
    then:
    noExceptionThrown()
    }

    View Slide

  95. interface Dao {
    Optional getMaybeById(long id)
    ...
    }
    @Ignore("Broken - Optional is final")
    def "should not fail on unstubbed call with Optional return type"() {
    given:
    Dao dao = Stub()
    when:
    dao.getMaybeById(5)
    then:
    noExceptionThrown()
    }
    CannotCreateMockException: Cannot create mock for class java.util.Optional
    because Java mocks cannot mock final classes.

    View Slide

  96. EmptyOrDummyResponse

    View Slide

  97. EmptyOrDummyResponse
    null
    ZeroOrNullResponse

    View Slide

  98. EmptyOrDummyResponse
    null
    ZeroOrNullResponse
    def "should not fail on unstubbed call with Optional return type - workaround 1"() {
    given:
    Dao dao = Stub([defaultResponse: ZeroOrNullResponse.INSTANCE])
    when:
    dao.getMaybeById(5)
    then:
    noExceptionThrown()
    }

    View Slide

  99. EmptyOrDummyResponse
    null
    ZeroOrNullResponse

    View Slide

  100. EmptyOrDummyResponse
    null
    ZeroOrNullResponse
    def "should not fail on unstubbed call with Optional return type - workaround 2"() {
    given:
    Dao dao = Mock() //instead of Stub()
    when:
    dao.getMaybeById(5)
    then:
    noExceptionThrown()
    }

    View Slide

  101. EmptyOrDummyResponse
    null
    ZeroOrNullResponse
    def "should not fail on unstubbed call with Optional return type - workaround 2"() {
    given:
    Dao dao = Mock() //instead of Stub()
    when:
    dao.getMaybeById(5)
    then:
    noExceptionThrown()
    }
    Optional

    View Slide

  102. View Slide

  103. class Java8EmptyOrDummyResponse implements IDefaultResponse {
    public static final Java8EmptyOrDummyResponse INSTANCE = new Java8EmptyOrDummyResponse();
    private Java8EmptyOrDummyResponse() {}
    @Override
    public Object respond(IMockInvocation invocation) {
    if (invocation.getMethod().getReturnType() == Optional) {
    return Optional.empty()
    }
    //possibly CompletableFutures.completedFuture(), dates and maybe others
    return EmptyOrDummyResponse.INSTANCE.respond(invocation)
    }
    }

    View Slide

  104. defaultResponse Stub
    def "should return empty Optional for unstubbed calls"() {
    given:
    Dao dao = Stub([defaultResponse: Java8EmptyOrDummyResponse.INSTANCE])
    when:
    Optional result = dao.getMaybeById(5)
    then:
    result?.isPresent() == false //NOT the same as !result?.isPresent()
    }

    View Slide

  105. defaultResponse Stub
    def "should return empty Optional for unstubbed calls"() {
    given:
    Dao dao = Stub([defaultResponse: Java8EmptyOrDummyResponse.INSTANCE])
    when:
    Optional result = dao.getMaybeById(5)
    then:
    result?.isPresent() == false //NOT the same as !result?.isPresent()
    }
    Dao dao = Stub8(Dao)
    T Stub8(Class clazz) {
    return Stub([defaultResponse: Java8EmptyOrDummyResponse.INSTANCE], clazz)
    }

    View Slide

  106. def "should change real method execution return value"() {
    given:
    TacticalStation tsSpy = Spy()
    and:
    tsSpy.getTubeStatus(_) >> {
    if (it[0] == 4) {
    return TubeStatus.BROKEN
    } else {
    return callRealMethod()
    }
    }
    expect:
    tsSpy.getTubeStatus(1) == TubeStatus.LOADED
    and:
    tsSpy.getTubeStatus(4) == TubeStatus.BROKEN
    }
    callRealMethod()

    View Slide

  107. def "should call real method with changed arguments"() {
    given:
    TacticalStation tsSpy = Spy()
    and:
    tsSpy.getTubeStatus(_) >> { int tubeNumber ->
    return callRealMethodWithArgs(tubeNumber + 1)
    }
    expect:
    ...
    }
    callRealMethodWithArgs()

    View Slide

  108. View Slide

  109. Assertion failed:
    assert word.substring(begin, end) == word[begin..end]
    | | | | | | || |
    | po 1 3 | | |1 3
    Spock | | poc
    | Spock
    false
    assert
    then expect

    View Slide

  110. Assertion failed:
    assert word.substring(begin, end) == word[begin..end]
    | | | | | | || |
    | po 1 3 | | |1 3
    Spock | | poc
    | Spock
    false
    assert
    then expect

    View Slide

  111. assert
    when:
    ...
    then:
    assertCompatibilityWithEU49683Directive(order)
    }
    private void assertCompatibilityWithEU49683Directive(Order order) {
    assert order.field1 == ...
    assert order.field2 == ...
    }

    View Slide

  112. assert
    when:
    ...
    then:
    assertCompatibilityWithEU49683Directive(order)
    }
    private void assertCompatibilityWithEU49683Directive(Order order) {
    assert order.field1 == ...
    assert order.field2 == ...
    }
    given
    given:
    int initialNumberOfFiles = getNumberOfFileInDirectory()
    assert initialNumberOfFiles == 0
    when:
    ...

    View Slide

  113. assert
    expect:
    GParsPool.withPool {
    ["1"].eachParallel CorrelationIdUpdater.wrapClosureWithId {
    assert it == "1"
    }
    }

    View Slide

  114. assert
    expect:
    GParsPool.withPool {
    ["1"].eachParallel CorrelationIdUpdater.wrapClosureWithId {
    assert it == "1"
    }
    }

    View Slide

  115. def "should show nice error message on null dereference"(){
    when:
    Person readPerson = dao.getById(TEST_WOMAN)
    then:
    readPerson.sex == Sex.WOMAN
    ...
    }

    View Slide

  116. def "should show nice error message on null dereference"(){
    when:
    Person readPerson = dao.getById(TEST_WOMAN)
    then:
    readPerson.sex == Sex.WOMAN
    ...
    }
    java.lang.NullPointerException: Cannot get property 'sex' on null object
    ?.

    View Slide

  117. def "should show nice error message on null dereference"(){
    when:
    Person readPerson = dao.getById(TEST_WOMAN)
    then:
    readPerson?.sex == Sex.WOMAN
    ...
    }
    readPerson?.sex == Sex.WOMAN
    | | |
    null | false
    null

    View Slide

  118. def "should show nice error message on null dereference"(){
    when:
    Person readPerson = dao.getById(TEST_WOMAN)
    then:
    readPerson.sex == Sex.WOMAN //wihtout '?.'
    ...
    }
    java.lang.NullPointerException: Cannot invoke method startsWith() on null object
    readPerson.sex == Sex.WOMAN
    | | |
    null | false
    java.lang.NullPointerException: Cannot get property 'sex' on null object

    View Slide

  119. @Stepwise

    View Slide

  120. View Slide

  121. View Slide

  122. View Slide

  123. View Slide

  124. View Slide

  125. View Slide

  126. View Slide