exception - Spock: probando excepciones con tablas de datos
testing groovy (6)
Aquí está la solución que se me ocurrió. Básicamente es la Variante 3, pero usa un bloque de try/catch
para evitar el uso de las condiciones de excepción de Spock (ya que esas deben ser de nivel superior).
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''
}
Algunas advertencias:
- Necesita varios bloques catch para probar diferentes excepciones.
- Debe usar condiciones explícitas (afirmaciones
assert
) dentro de bloques try / catch. - No puedes separar tus estímulos y tus respuestas en bloques de
when-then
.
¿Cómo se pueden probar las excepciones de una manera agradable (por ejemplo, tablas de datos) con Spock?
Ejemplo: Tener un método validateUser
que pueda lanzar excepciones con diferentes mensajes o sin excepción si el usuario es válido.
La clase de especificación en sí misma:
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'')
}
}
Variante 1
Este funciona, pero la verdadera intención está atestada de todas las etiquetas when / then y las llamadas repetidas de 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''
}
Variante 2
Éste no funciona debido a este error planteado por Spock en el momento de la compilación:
Las condiciones de excepción solo se permiten en bloques ''entonces''
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'' }
}
Variante 3
Éste no funciona debido a este error planteado por Spock en el momento de la compilación:
Las condiciones de excepción solo se permiten como declaraciones de nivel superior
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''
}
Aquí hay un ejemplo de cómo lo @Unroll
usando @Unroll
y when:
then:
y where:
bloques. Se ejecuta utilizando las 3 pruebas con los datos de la tabla de datos:
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'' | _
}
}
Así es como lo hago, modifico la cláusula when:
para lanzar siempre una excepción de Success
, de esa manera no necesita pruebas separadas o lógica para decir si se llama notThrown
o no notThrown
, simplemente siempre se thrown
llamada con la tabla de datos que indica si esperar el Success
o no.
Podría cambiar el nombre de Success
para que sea None
o NoException
o lo que prefiera.
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'')
}
}
Una cosa adicional que cambiaría, sería usar una subclase para las excepciones de falla también para evitar que se Success
accidentalmente el Success
cuando realmente esperaba una falla. No afecta su ejemplo porque tiene una verificación adicional para el mensaje, pero otras pruebas pueden simplemente probar el tipo de excepción.
class Failure extends Exception {}
y use esa o alguna otra excepción "real" en lugar de la Exception
vanilla
La solución recomendada es tener dos métodos: uno que pruebe los casos buenos y otro que pruebe los casos malos. Entonces ambos métodos pueden hacer uso de tablas de datos.
Ejemplo:
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'')
}
}
Puede ajustar su llamada de método con un método que devuelva el mensaje o la clase de excepción, o un mapa de ambos ...
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
}
}
Usando el ejemplo de @AmanuelNega, intenté esto en la consola web de spock y guardé el código en 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
}
}
}