Search code examples
groovyspock

Spock - running data table count


Is there any method in Spock to transparently get the current running count when using data tables without the the need to have it as an explicit input parameter?

For example

class MathSpec extends Specification {
    def "maximum of two numbers"(int a, int b, int c) {
        expect:
        Math.max(a, b) == c
        println "this is row:" + $currentRowCount//??


        where:
        a | b | c
        1 | 3 | 3
        7 | 4 | 4
        0 | 0 | 0
    }
}

Solution

  • This is really ugly because iterationCount is a private variable, but possible:

    Example spec:

    package de.scrum_master.app
    
    import spock.lang.Specification
    import spock.lang.Unroll
    
    class PieceTest extends Specification {
      @Unroll
      def "do something with data table item #item"() {
        expect:
        println specificationContext
          .currentIteration
          .parent
          .iterationNameProvider
          .iterationCount
    
        where:
        item  | anotherItem
        "foo" | 333
        "bar" | 444
        "zot" | 555
      }
    }
    

    Console log:

    1
    2
    3
    

    Please note that this only works for @Unrolled feature methods, not without that annotation.


    Update: Expose iteration count via global Spock extension

    Disclaimer: I love Spock but I am not fluent in Groovy meta programming. This is also my first Spock extension.

    You may know built-in Spock extensions which are mostly annotation-driven, such as @Ignore, @Timeout, @Stepwise, @Issue, @AutoCleanup. But there are also global extensions which are not described in the manual but still exist, e.g. ReportLogExtension, IncludeExcludeExtension.

    Unfortunately the manual does not describe how to create such extensions, but with some googling and source code research you can find out. It is nothing for beginners, though.

    Global Spock extension:

    This extension adds a dynamic member iterationCount to each Spock specification.

    package de.scrum_master.app
    
    import org.spockframework.runtime.AbstractRunListener
    import org.spockframework.runtime.extension.AbstractGlobalExtension
    import org.spockframework.runtime.model.FeatureInfo
    import org.spockframework.runtime.model.IterationInfo
    import org.spockframework.runtime.model.SpecInfo
    
    class IterationCountExtension extends AbstractGlobalExtension {
      @Override
      void visitSpec(SpecInfo spec) {
        spec.addListener(new IterationCountListener())
      }
    
      static class IterationCountListener extends AbstractRunListener {
        MetaClass metaClass
        int iterationCount
    
        @Override
        void beforeSpec(SpecInfo spec) {
          println spec.name
          metaClass = spec.reflection.metaClass
        }
    
        @Override
        void beforeFeature(FeatureInfo feature) {
          println "  " + feature.name
          iterationCount = 0
          metaClass.iterationCount = iterationCount
        }
    
        @Override
        void beforeIteration(IterationInfo iteration) {
          println "    " + iteration.name
          metaClass.iterationCount = iterationCount++
        }
      }
    }
    

    Each extension needs to be registered. So please also add a file META-INF/services/org.spockframework.runtime.extension.IGlobalExtension with the following content to your test resources:

    de.scrum_master.app.IterationCountExtension
    

    Showcase specification:

    package de.scrum_master.app
    
    import spock.lang.Specification
    import spock.lang.Unroll
    
    class SampleTest extends Specification {
      def "no data table"() {
        expect:
        println "      " + iterationCount
      }
    
      def "data table items"() {
        expect:
        println "      " + iterationCount
    
        where:
        item  | anotherItem
        "foo" | 333
        "bar" | 444
        "zot" | 555
      }
    
      @Unroll
      def "unrolled data table item"() {
        expect:
        println "      " + iterationCount
    
        where:
        item  | anotherItem
        "foo" | 333
        "bar" | 444
        "zot" | 555
      }
    
      @Unroll
      def "unrolled data table item #item"() {
        expect:
        println "      " + iterationCount
    
        where:
        item  | anotherItem
        "foo" | 333
        "bar" | 444
        "zot" | 555
      }
    }
    

    Console log:

    SampleTest
      no data table
        no data table
          0
      data table items
        data table items
          0
        data table items
          1
        data table items
          2
      unrolled data table item
        unrolled data table item[0]
          0
        unrolled data table item[1]
          1
        unrolled data table item[2]
          2
      unrolled data table item #item
        unrolled data table item foo
          0
        unrolled data table item bar
          1
        unrolled data table item zot
          2
    

    As you can see, the extension works regardless of whether @Unroll is used or not. In this respect it is also better than the quick & dirty solution from above.

    BTW, if you want the count to be 1-based instead of 0-based, just switch the expression iterationCount++ to ++iterationCount in the extension listener method beforeIteration.