Spock - Исключения для тестирования с таблицами данных

Как исключения могут быть протестированы с хорошим доступом (например, таблицы данных) с помощью Spock?

Пример: наличие метода validateUser, который может генерировать исключения с разными сообщениями или исключение, если пользователь действителен.

Сам класс спецификации:

class User { String userName }

class SomeSpec extends spock.lang.Specification {

    ...tests go here...

    private validateUser(User user) {
        if (!user) throw new Exception ('no user')
        if (!user.userName) throw new Exception ('no userName')
    }
}

Вариант 1

Этот работает, но реальное намерение загромождает все метки /then и повторные вызовы validateUser(user).

    def 'validate user - the long way - working but not nice'() {
        when:
        def user = new User(userName: 'tester')
        validateUser(user)

        then:
        noExceptionThrown()

        when:
        user = new User(userName: null)
        validateUser(user)

        then:
        def ex = thrown(Exception)
        ex.message == 'no userName'

        when:
        user = null
        validateUser(user)

        then:
        ex = thrown(Exception)
        ex.message == 'no user'
    }

Вариант 2

Этот не работает из-за этой ошибки, вызванной Spock во время компиляции:

Условия исключения разрешены только в блоках "then"

    def 'validate user - data table 1 - not working'() {
        when:
        validateUser(user)

        then:
        check()

        where:
        user                         || check
        new User(userName: 'tester') || { noExceptionThrown() }
        new User(userName: null)     || { Exception ex = thrown(); ex.message == 'no userName' }
        null                         || { Exception ex = thrown(); ex.message == 'no user' }
    }

Вариант 3

Этот не работает из-за этой ошибки, вызванной Spock во время компиляции:

Условия исключения разрешены только в качестве операторов верхнего уровня

    def 'validate user - data table 2 - not working'() {
        when:
        validateUser(user)

        then:
        if (expectedException) {
            def ex = thrown(expectedException)
            ex.message == expectedMessage
        } else {
            noExceptionThrown()
        }

        where:
        user                         || expectedException | expectedMessage
        new User(userName: 'tester') || null              | null
        new User(userName: null)     || Exception         | 'no userName'
        null                         || Exception         | 'no user'
    }

Ответ 1

Рекомендуемое решение состоит в том, чтобы иметь два метода: один, который проверяет хорошие случаи, и другой, который проверяет плохие случаи. Тогда оба метода могут использовать таблицы данных.

Пример:

class SomeSpec extends Specification {

    class User { String userName }

    def 'validate valid user'() {
        when:
        validateUser(user)

        then:
        noExceptionThrown()

        where:
        user << [
                new User(userName: 'tester'),
                new User(userName: 'joe')]
    }

    def 'validate invalid user'() {
        when:
        validateUser(user)

        then:
        def error = thrown(expectedException)
        error.message == expectedMessage

        where:
        user                     || expectedException | expectedMessage
        new User(userName: null) || Exception         | 'no userName'
        new User(userName: '')   || Exception         | 'no userName'
        null                     || Exception         | 'no user'
    }

    private validateUser(User user) {
        if (!user) throw new Exception('no user')
        if (!user.userName) throw new Exception('no userName')
    }

}

Ответ 2

Вы можете связать вызов метода с методом, который возвращает сообщение или класс исключения, или карту обоих...

  def 'validate user - data table 2 - not working'() {
        expect:
            expectedMessage == getExceptionMessage(&validateUser,user)
        where:
        user                         || expectedMessage
        new User(userName: 'tester') || null
        new User(userName: null)     || 'no userName'
        null                         || 'no user'
    }

    String getExceptionMessage(Closure c, Object... args){
        try{
            return c.call(args)
            //or return null here if you want to check only for exceptions
        }catch(Exception e){
            return e.message
        }
    }

Ответ 3

Используя пример из @AmanuelNega, я попробовал это на веб-консоли spock и сохранил код http://meetspock.appspot.com/script/5713144022302720

import spock.lang.Specification

class MathDemo {
    static determineAverage(...values) 
      throws IllegalArgumentException {
        for (item in values) {
            if (! (item instanceof Number)) {
                throw new IllegalArgumentException()
            }
        }

        if (!values) {
            return 0
        }

        return values.sum() / values.size()
    }
}

class AvgSpec extends Specification {

    @Unroll
    def "average of #values gives #result"(values, result){
        expect:
            MathDemo.determineAverage(*values) == result

        where:
            values       || result
            [1,2,3]      || 2
            [2, 7, 4, 4] || 4.25
            []           || 0
    }

    @Unroll
    def "determineAverage called with #values throws #exception"(values, exception){
        setup:
           def e = getException(MathDemo.&determineAverage, *values)

        expect:
            exception == e?.class

        where:
            values       || exception
            ['kitten', 1]|| java.lang.IllegalArgumentException
            [99, true]   || java.lang.IllegalArgumentException
            [1,2,3]      || null
    }

    Exception getException(closure, ...args){
        try{
            closure.call(args)
            return null
        } catch(any) {
            return any
        }
    }
}
​

Ответ 4

Вот решение, с которым я столкнулся. Это в основном вариант 3, но он использует блок try/catch, чтобы избежать использования условий исключения Спока (так как они должны быть верхнего уровня).

def "validate user - data table 3 - working"() {
    expect:
    try {
        validateUser(user)
        assert !expectException
    }
    catch (UserException ex)
    {
        assert expectException
        assert ex.message == expectedMessage
    }

    where:
    user                         || expectException | expectedMessage
    new User(userName: 'tester') || false           | null
    new User(userName: null)     || true            | 'no userName'
    null                         || true            | 'no user'
}

Некоторые оговорки:

  • Вам нужно несколько блоков catch для проверки различных исключений.
  • Вы должны использовать явные условия (assert statements) внутри блоков try/catch.
  • Вы не можете отделить свои стимулы и ответы от блоков when-then.

Ответ 5

Вот пример того, как я достиг этого, используя блоки @Unroll и when:, then: и where:. Он запускается с использованием всех трех тестов с данными из таблицы данных:

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

import java.util.regex.Pattern

class MyVowelString {
    private static final Pattern HAS_VOWELS = Pattern.compile('[aeiouAEIOU]')
    final String string

    MyVowelString(String string) {
        assert string != null && HAS_VOWELS.matcher(string).find()
        this.string = string
    }
}

class PositiveNumberTest extends Specification {
    @Unroll
    def "invalid constructors with argument #number"() {
        when:
        new MyVowelString(string)

        then:
        thrown(AssertionError)

        where:
        string | _
        ''     | _
        null   | _
        'pppp' | _
    }
}

Ответ 6

Другой подход заключается в возврате после проверки сценария без исключений, поэтому не вложен в другой сценарий:

def 'validate user - data table 2 - not working'() {
    when:
        validateUser(user)

    then:
        if (!expectedException) {
            noExceptionThrown()

            return
        }

        def ex = thrown(expectedException)
        ex.message == expectedMessage

    where:
        user                         || expectedException | expectedMessage
        new User(userName: 'tester') || null              | null
        new User(userName: null)     || Exception         | 'no userName'
        null                         || Exception         | 'no user'
}

Единственный улов, который вы не можете использовать notThrown в методе noExceptionThrown, но это немного менее громоздко, чем разделение теста, и его можно исправить, проверив вместо этого счастливый путь.

Ответ 7

Здесь, как я это делаю, я изменяю предложение when:, чтобы всегда генерировать исключение Success, поэтому вам не нужны отдельные тесты или логика, чтобы сказать, следует ли вызывать thrown или notThrown, просто всегда вызывайте thrown с таблицей данных, указывающей, следует ли ожидать Success или нет.

Вы можете переименовать Success как None или NoException или что угодно.

class User { String userName }

class SomeSpec extends spock.lang.Specification {

    class Success extends Exception {}

    def 'validate user - data table 2 - working'() {
        when:
            validateUser(user)
            throw new Success ()

        then:
            def ex = thrown(expectedException)
            ex.message == expectedMessage

        where:
            user                         || expectedException | expectedMessage 
            new User(userName: 'tester') || Success           | null
            new User(userName: null)     || Exception         | 'no userName'
            null                         || Exception         | 'no user'
    }

    private validateUser(User user) {
        if (!user) throw new Exception ('no user')
        if (!user.userName) throw new Exception ('no userName')
    }
}

Одна дополнительная вещь, которую я бы изменил, заключалась бы в том, чтобы использовать подкласс для исключений сбоев, чтобы избежать случайного попадания Success, когда вы действительно ожидали провала. Это не влияет на ваш пример, потому что у вас есть дополнительная проверка для сообщения, но другие тесты могут просто проверить тип исключения.

class Failure extends Exception {}

и используйте это или какое-то другое "реальное" исключение вместо vanilla Exception