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