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. 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 full-size slide

  2. 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 full-size slide

  3. 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 full-size slide

  4. @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 full-size slide

  5. @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 full-size slide

  6. 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 full-size slide

  7. 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 full-size slide

  8. 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 full-size slide

  9. 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 full-size slide

  10. //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 full-size slide

  11. //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 full-size slide

  12. //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 full-size slide

  13. setup/cleanup

    View full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  26. +---------------+
    | Specification |
    +---------------+

    View full-size slide

  27. +---------------+
    | Specification |
    +---------------+
    ^
    |
    +---------------------+
    | BaseIntegrationSpec |
    +---------------------+

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  35. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. @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 full-size slide

  40. @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 full-size slide

  41. @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 full-size slide

  42. @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 full-size slide

  43. @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 full-size slide

  44. @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 full-size slide

  45. //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 full-size slide

  46. @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 full-size slide

  47. @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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. @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 full-size slide

  60. 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 full-size slide

  61. @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 full-size slide

  62. 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 full-size slide

  63. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. EmptyOrDummyResponse

    View full-size slide

  72. EmptyOrDummyResponse
    null
    ZeroOrNullResponse

    View full-size slide

  73. 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 full-size slide

  74. EmptyOrDummyResponse
    null
    ZeroOrNullResponse

    View full-size slide

  75. 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 full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

  78. 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 full-size slide

  79. 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 full-size slide

  80. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  85. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  89. 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 full-size slide

  90. 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 full-size slide

  91. 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 full-size slide