2

It seems a reasonable thing to want to do... e.g. log something when a test fails, and not if it doesn't.

I've found this, for example, from 2013... there were no both simple and effective answers at the time. What about now?

I hoped a suitable property/method might be found in org.spockframework.runtime.SpecificationContext ... or maybe org.spockframework.runtime.model.SpecInfo ... but I can't see anything.

later

To answer the question of the sort of thing I might want to do: in fact my Specifications "hijack" System.out (using a PrintStream) so I can capture output to System.out and then analyse it. NB for the record I'm aware Spock purists may not approve of tests taking any interest in terminal output, but I'm not such a purist, specifically when talking about tests other than unit tests.

Having obtained this output in a way which means it is not output anywhere there is no reason to log it systematically and clutter up the log file ... but if a test fails I want to do so. Ditto potentially for System.err...

mike rodent
  • 14,126
  • 11
  • 103
  • 157

1 Answers1

3

First of all, logging failed tests is your test framework's (JUnit, Spock) job. So it is no big surprise that the status of the test itself is not readily available from within that test itself. Anyway, if you want something fancier in Spock, the accepted answer and also Peter's answer in the other thread are still valid. There is no simpler way of finding out whether a test has failed or not from the cleanup() method.

Anyway, it is not as complicated as it looks because you only set it up once and then it just works. As you have not mentioned what exactly you want to log in cleanup(), I am going to speculate a little bit. For demo purposes I am just logging the feature method name, retrieved from the specification context, and the class of the error that has occurred (so as not to print the whole fancy Spock error message twice), retrieved from a run listener registered by the global extension I am going to present here.

Global Spock extension:

The extension registers a run listener which records the error info whenever a test error occurs. At the beginning of each feature or iteration (for features with where: blocks) the last recorded error is cleared so as not to bleed into the next feature/iteration.

package de.scrum_master.testing.extension

import org.spockframework.runtime.AbstractRunListener
import org.spockframework.runtime.extension.AbstractGlobalExtension
import org.spockframework.runtime.model.ErrorInfo
import org.spockframework.runtime.model.IterationInfo
import org.spockframework.runtime.model.SpecInfo

class TestResultExtension extends AbstractGlobalExtension {
  @Override
  void visitSpec(SpecInfo spec) {
    spec.addListener(new ErrorListener())
  }

  static class ErrorListener extends AbstractRunListener {
    ErrorInfo errorInfo

    @Override
    void beforeIteration(IterationInfo iteration) {
      errorInfo = null
    }

    @Override
    void error(ErrorInfo error) {
      errorInfo = error
    }
  }
}

How to register the Spock extension:

You also need to add the file META-INF/services/org.spockframework.runtime.extension.IGlobalExtension to your test resources in order to register the extension. The file simply has this content:

de.scrum_master.testing.extension.TestResultExtension

BTW, this is not a Spock or Groovy thing but a standard Java SE feature called service providers.

Sample test using the extension:

This test is pretty stupid but shows how this works for normal methods and methods with where: blocks both with and without @Unroll.

package de.scrum_master.testing.extension

import spock.lang.Specification
import spock.lang.Unroll

class TestFailureReportingTest extends Specification {
  def "failing normal feature"() {
    expect:
    0 == 1
  }

  def "passing normal feature"() {
    expect:
    0 == 0
  }

  def "parametrised feature"() {
    expect:
    a == b

    where:
    a << [2, 4, 6]
    b << [3, 5, 6]
  }

  @Unroll
  def "unrolled feature with #a/#b"() {
    expect:
    a == b

    where:
    a << [6, 8, 0]
    b << [7, 9, 0]
  }

  def cleanup() {
    specificationContext.currentSpec.listeners
      .findAll { it instanceof TestResultExtension.ErrorListener }
      .each {
        def errorInfo = (it as TestResultExtension.ErrorListener).errorInfo
        if (errorInfo)
          println "Test failure in feature '${specificationContext.currentIteration.name}', " +
            "exception class ${errorInfo.exception.class.simpleName}"
        else
          println "Test passed in feature '${specificationContext.currentIteration.name}'"
      }
  }
}

The console log (omitting the actual errors) would be:

Test failure in feature 'failing normal feature', exception class ConditionNotSatisfiedError

Test passed in feature 'passing normal feature'

Test failure in feature 'parametrised feature', exception class ConditionNotSatisfiedError
Test failure in feature 'parametrised feature', exception class ConditionNotSatisfiedError
Test passed in feature 'parametrised feature'

Test failure in feature 'unrolled feature with 6/7', exception class ConditionNotSatisfiedError
Test failure in feature 'unrolled feature with 8/9', exception class ConditionNotSatisfiedError
Test passed in feature 'unrolled feature with 0/0'

P.S.: The error info is not available yet in a cleanup: block inside a feature method because the extension only kicks in after the whole feature/iteration including that block has finished running. So you really have to use a cleanup() method, but you wanted that anyway and it avoids code duplication.

P.P.S.: Of course you could also just do generic logging from within the method interceptor and skip the whole cleanup() method. But then you no longer can make your log output test-specific and it will be there for all your tests, not just the one you choose - unless of course you hard-code a package or specification name filter right into the interceptor or make sure the interceptor reads a corresponding config file when Spock starts up.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • 2
    "it is not as complicated as it looks" ... er, perhaps! All I can say is that would have taken me perhaps a day to work out. Also, vielen Dank ! – mike rodent Jun 04 '18 at 13:41
  • I found this again in another context and updated the sample code: The test prints the unrolled method (iteration) name now in case you use `@Unroll` with parameter name placeholders. I also removed the superfluous method `beforeFeature` from the interceptor. – kriegaex May 04 '20 at 02:31