exceptiongroovyspockdata-driven-tests

Spock testing exception handling in a Where block


I am testing a service method which has a few dependencies; and I want to assert that if any of these dependencies throws an exception, the service method should return a default value.

The service and the test I'd like to write look something like this.

static class Service {
    def dependency1
    def dependency2
    def dependency3

    def method() {
        try {
            def foo = dependency1.get()
            def bar = dependency2.get()
            def baz = dependency3.get()
            return " $foo $bar $baz "
        } catch (Exception e) {
            println e
            return ' default value '
        }
    }
}

def 'test Service error handling'() {
    given:
    def dependency1 = Mock(Supplier)
    def dependency2 = Mock(Supplier)
    def dependency3 = Mock(Supplier)
    def serviceUnderTest = new Service(dependency1: dependency1, dependency2: dependency2, dependency3: dependency3)

    when:
    def result = serviceUnderTest.method()

    then:
    result == ' default value '
    dependency1.get() >> closure1
    dependency2.get() >> closure2
    dependency3.get() >> closure3

    where:
    closure1                              | closure2                              | closure3
    {-> throw new Exception('closure1') } | {-> null }                            | {-> null };
    {-> null}                             | {-> throw new Exception('closure2') } | {-> null };
    {-> null}                             | {-> null}                             | {-> throw new Exception('closure3') }
}

This test doesn't work, because it causes the mocks to return literal closures rather than the results of those closures. Of course this is caused by the addition of the where block, as any mock can directly return the result of a single closure, i.e. dependency1.get() >> { throw new Exception() }

Am I forced to write this as three separate tests, or is there another way I can combine them?


Solution

  • If you write

    dependency1.get() >> closure1
    

    your mock will return the closure itself and not evaluate it. The evaluation only happens inside the GroovyString " $foo $bar $baz ", expanding the error message happening during evaluation into it but not escalating that exception.

    You want to use

    dependency1.get() >> { closure1() }
    

    in order to fix your test. The () evaluates your closure, but at the same time the surrounding closure {} makes sure that the evaluation happens only when the stub method is called, not already when it is defined.

    A few improvement ideas:

    package de.scrum_master.stackoverflow.q57172322
    
    import spock.lang.Specification
    import spock.lang.Unroll
    
    class ServiceDependenciesThrowingErrorsTest extends Specification {
    
      @Unroll
      def 'handle error in service #serviceName'() {
        given:
        def serviceUnderTest = new Service(
          dependency1: Mock(Supplier) { get() >> { closure1() } },
          dependency2: Mock(Supplier) { get() >> { closure2() } },
          dependency3: Mock(Supplier) { get() >> { closure3() } }
        )
    
        expect:
        serviceUnderTest.method() == 'default value'
    
        where:
        serviceName | closure1                            | closure2                            | closure3
        "A"         | { throw new Exception('closure1') } | { null }                            | { null }
        "B"         | { null }                            | { throw new Exception('closure2') } | { null }
        "C"         | { null }                            | { null }                            | { throw new Exception('closure3') }
      }
    
      static class Service {
        def dependency1
        def dependency2
        def dependency3
    
        def method() {
          try {
            def foo = dependency1.get()
            def bar = dependency2.get()
            def baz = dependency3.get()
            return "$foo $bar $baz"
          } catch (Exception e) {
            println e
            return 'default value'
          }
        }
      }
    
      static class Supplier {
        def get() {
          "OK"
        }
      }
    
    }
    

    This is what the test execution looks like in my IDE (IntelliJ IDEA) when unrolled:

    Unrolled test execution in IntelliJ IDEA