Spock - 使用数据表测试异常
Posted
技术标签:
【中文标题】Spock - 使用数据表测试异常【英文标题】:Spock - Testing Exceptions with Data Tables 【发布时间】:2013-10-11 17:02:27 【问题描述】:如何使用 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
这个是有效的,但真正的意图被所有 when / 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'
【问题讨论】:
上周遇到了同样的情况,我完全按照@peter 的建议做了。 :) 基于一个数据表处理两种异常变体(抛出/未抛出)不是方法。你甚至不能在数据表中抛出异常。 【参考方案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')
【讨论】:
我记得一个问题,如果MyException
没有被抛出,thrown(MyException)
不能返回null吗?
我将不得不重新审视我的测试。但是我在数据表中使用 throw()/notThrown() 时遇到了错误。无论如何感谢一个精彩的测试框架。因为你,我在工作中成为了“that-BDD-developer”。 ;)
我可能会以这种方式工作,但目前您不能将null
传递给thrown()
。
@PeterNiederwieser 这对于如何处理数据表中的异常的基本示例非常有用。这是 Google 上“spock 数据异常”的热门话题,参考示例(或指向 doc)会非常有帮助,谢谢。【参考方案2】:
这是我想出的解决方案。它基本上是变体 3,但它使用 try/catch
块来避免使用 Spock 的异常条件(因为那些 必须是***的)。
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 块来测试不同的异常。
您必须在 try/catch 块中使用显式条件(
assert
语句)。
您不能将刺激和响应分成when-then
块。
【讨论】:
非常适合我的情况。我刚刚更新为仅在提供消息时检查异常:assert !exceptionMessage
,并且可以删除expectException
列。【参考方案3】:
您可以使用返回消息或异常类的方法或两者的映射来包装您的方法调用...
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
【讨论】:
【参考方案4】:我是这样做的,我将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
并使用该异常或其他一些“真实”异常而不是原版 Exception
【讨论】:
IMO,抛出表示成功的异常是一种非常糟糕的代码气味。请参阅有效的 Java 第 69 条:仅在异常情况下使用异常。 它只是在测试中被抛出,以解决框架的限制,因此 IMO 这种类型的启发式方法不适用,或者被它掩盖的其他气味所压倒。跨度> 【参考方案5】:使用来自 @AmanuelNega 的示例,我在 spock Web 控制台上尝试了此操作,并将代码保存在 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
【讨论】:
【参考方案6】:我有不会扭曲您的测试工作流程的解决方案,您可以通过放置在 where 表中的动态对象的内容来分析异常
@Unroll
def "test example [a=#a, b=#b]"()
given:
def response
def caughtEx
when:
try
result = someAmazingFunctionWhichThrowsSometimes(a,b)
catch (Exception ex)
caughtEx = ex
then:
result == expected
if (exception.expected)
assert caughtEx != null && exception.type.isInstance(caughtEx)
else
assert caughtEx == null
where:
a | b || exception | expected
8 | 4 || [expected: false] | 2
6 | 3 || [expected: false] | 3
6 | 2 || [expected: false] | 3
4 | 0 || [expected: true, type: RuntimeException] | null
【讨论】:
【参考方案7】:这是我如何使用@Unroll
和when:
、then:
和where:
块实现它的示例。它使用数据表中的数据运行所有 3 个测试:
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' | _
【讨论】:
以上是关于Spock - 使用数据表测试异常的主要内容,如果未能解决你的问题,请参考以下文章