No hay forma, por más que pruebas bibliotecas Java de tests no encuentras una forma de hacerlos sencilla y potente y que realmente te convenza del todo. Tu código de pruebas a menudo acaba siendo un pequeño batiburrillo ilegible que prefieres tocar lo mínimo posible, y que incluso te quita las ganas de hacer más tests. Has oído que la gente de Groovy habla muy bien de Spock, pero no te fías mucho de esos paganos que hacen guarreos con el código y no adoran debidamente al gran dios J.
En esta charla, que se presentó el 26/6/2014 organizada por MadridJUG y MadridGUG, Andrés Viedma nos mostrará lo sencillo que es integrar Spock en un proyecto Java, y cómo su gran expresividad y la potencia de Groovy nos pueden ayudar a crear tests en los que puedas preocuparte más de qué quieres probar que de cómo tienes que programar la prueba, y que además sirvan para documentar de una forma muy elegante cuál es el comportamiento esperado de nuestra querida aplicación Java. Se explorará además cómo Spock puede encajar perfectamente no solo con TDD sino también con la automatización de tests funcionales sobre la web, e incluso con técnicas de más alto nivel como BDD y metodologías ágiles en general.
2. ¿Quién soy?¿Quién soy?
Dinosaurio del software
más de 20 años como profesional
Javero inquieto
Sospechoso habitual del MadridGUG y
MadridJUG
Escribo en Apaga y vuelve a encender
http://apagayvuelveaencender.blogspot.com
Andrés ViedmaAndrés Viedma
@andres_viedma@andres_viedma
14. Tests a prueba de VagosTests a prueba de Vagos
Subir nivel de abstracción
No “programar tests” → declarar casos de prueba
Sencillez + potencia
Expresividad → test es a la vez documentación
Fácil de ejecutar en sistemas de integración continua y
en IDEs
Información que facilite la detección de errores
18. SPOCKSPOCK
Muy parecido a Java (“extensión” del lenguaje)
Compatible con él (se ejecuta en JVM)
Lenguaje dinámico (o no)
Mucho “azúcar sintáctico”
Mucha “magia negra”
Diseñado para maximizar sencillez y expresividad
Tiene su propio runner JUnit
Hecho en Groovy
28. ¿IDEs?¿IDEs?
Groovy Eclipse Plugin
– http://groovy.codehaus.org/Eclipse+Plugin
Versiones Eclipse entre 3.5 (Galileo) y 4.3 (Kepler)
Instalar versión adecuada (Extra Groovy compilers – 2.1)
Plugin Groovy incluido en instalación
29. ¿Nada más?¿Nada más?
¡Nada más!
SDK Groovy no hace falta
Requisitos mínimos
JDK 5.0
Probado con Maven 2.0.9 (última 3.2.1...)
Eclipse Galileo
No requiere cambios importantes en entorno de desarrollo
31. import spock.lang.Specification;
class SillySpec extends Specification {
def "add two numbers"() {
expect:
1 + 1 == 2
}
}
El test más tonto del mundoEl test más tonto del mundo
src/test/groovy/SillySpec.groovy
32. import spock.lang.Specification;
class SillySpec extends Specification {
def "add two numbers"() {
expect:
1 + 1 == 2
}
}
El test más tonto del mundoEl test más tonto del mundo
src/test/groovy/SillySpec.groovy
33. import spock.lang.Specification;
class SillySpec extends Specification {
def "add two numbers"() {
expect:
1 + 1 == 2
}
}
El test más tonto del mundoEl test más tonto del mundo
“Assert” implícito
src/test/groovy/SillySpec.groovy
34. El segundo test más tonto del mundoEl segundo test más tonto del mundo
def "add elements to a list"() {
given:
def list = ["one", "two"]
when:
list.add("three")
list << “four”
then:
list == ["one", "two", "three", "four"]
}
35. El segundo test más tonto del mundoEl segundo test más tonto del mundo
def "add elements to a list"() {
given:
def list = ["one", "two"]
when:
list.add("three")
list << “four”
then:
list == ["one", "two", "three", "four"]
}
36. El segundo test más tonto del mundoEl segundo test más tonto del mundo
def "add elements to a list"() {
given:
def list = ["one", "two"]
when:
list.add("three")
list << “four”
then:
list == ["one", "two", "three", "four"]
}
37. El segundo test más tonto del mundoEl segundo test más tonto del mundo
def "add elements to a list"() {
given:
def list = ["one", "two"]
when:
list.add("three")
list << “four”
then:
list == ["one", "two", "three", "four"]
}
DSL
38. El segundo test más tonto del mundoEl segundo test más tonto del mundo
def "add elements to a list"() {
given:
def list = ["one", "two"]
when:
list.add("three")
list << “four”
then:
list == ["one", "two", "three", "four"]
}
equals
Tipos opcionales Collection literals
; opcional
39. Organización en BloquesOrganización en Bloques
given (setup)
when
then
expect
where
cleanup
Estímulo /
respuesta
Comprobación
directa
and: encadenar varios
bloques del mismo
tipo
40. Organización en BloquesOrganización en Bloques
given (setup)
when
then
expect
where
cleanup
Estímulo /
respuesta
Comprobación
directa
Legibilidad
When/then: efectos laterales
Expect: método funcional puro
and: encadenar varios
bloques del mismo
tipo
41. Condiciones then / expectCondiciones then / expect
when:
stack.push(elem)
then:
!stack.empty
stack.size() == 1
stack.peek() == elem
Condiciones booleanas
sencillas
when:
stack.pop()
then:
thrown(EmptyStackException)
stack.empty
Condiciones excepciones
thrown / notThrown
Interacciones (...)
Sólo pueden contener condiciones o definición de variables
45. Tests como documentaciónTests como documentación
@Issue("http://www.mybugtracking.com/BUG-012324")
def "add elements to a list"() {
given: "a list with elements”
def list = ["one", "two"]
when: "two more are added”
list.add("three")
list << “four”
then: "the list includes now both elements”
list == ["one", "two", "three", "four"]
}
46. Tests como documentaciónTests como documentación
@Issue("http://www.mybugtracking.com/BUG-012324")
def "add elements to a list"() {
given: "a list with elements”
def list = ["one", "two"]
when: "two more are added”
list.add("three")
list << “four”
then: "the list includes now both elements”
list == ["one", "two", "three", "four"]
}
47. Tests como documentaciónTests como documentación
@Issue("http://www.mybugtracking.com/BUG-012324")
def "add elements to a list"() {
given: "a list with elements”
def list = ["one", "two"]
when: "two more are added”
list.add("three")
list << “four”
then: "the list includes now both elements”
list == ["one", "two", "three", "four"]
}
Comportamiento queda
mejor documentado
48. Tests como documentaciónTests como documentación
@Issue("http://www.mybugtracking.com/BUG-012324")
def "add elements to a list"() {
given: "a list with elements”
def list = ["one", "two"]
when: "two more are added”
list.add("three")
list << “four”
then: "the list includes now both elements”
list == ["one", "two", "three", "four"]
}
Comportamiento queda
mejor documentado
Bueno para razonamiento
TDD
49. Cambio de estadoCambio de estado
def "generate a sequential identifier"() {
given:
def gen = new SequentialIdGenerator()
when:
def id = gen.generateId()
then:
id == old(gen.nextId)
gen.nextId == old(gen.nextId) + 1
}
50. Cambio de estadoCambio de estado
def "generate a sequential identifier"() {
given:
def gen = new SequentialIdGenerator()
when:
def id = gen.generateId()
then:
id == old(gen.nextId)
gen.nextId == old(gen.nextId) + 1
}
51. Cambio de estadoCambio de estado
def "generate a sequential identifier"() {
given:
def gen = new SequentialIdGenerator()
when:
def id = gen.generateId()
then:
id == old(gen.nextId)
gen.nextId == old(gen.nextId) + 1
}
Ojo: no usar si el resultado
es un objeto mutable
53. Control de la EjecuciónControl de la Ejecución
@Ignore
def "esta no se va a ejecutar"() { }
@Ignore(reason = "porque no funciona ni p'atrás")
def "esta tampoco se va a ejecutar"() { }
@IgnoreRest
def "si lo pongo esta va a ser la única en ejecutarse"() { }
@IgnoreIf({ os.windows })
def "esta solo se ejecutaría en Windows"() { }
@Stepwise
class RunInOrderSpec extends Specification {
def "Este será siempre el primero"() { ... }
def "Este se ejecutará el segundo"() { ... }
}
@Timeout(5)
def "Falla si tarda más de 5 segundos"() { }
Ejecución
selectiva
Timeout
Orden de
ejecución
65. ¿Por qué “test doubles”?¿Por qué “test doubles”?
Problema: test de clase A que usa otra clase B que no
queremos probar:
Porque utiliza recursos externos (BD, APIs externas...)
Para independizar las pruebas
“Test doubles” reemplazan la clase B por objetos “de pega”
Stub: devuelve respuestas prefijadas en el test
Mock: cascarón vacío con respuestas por defecto
Spy: pone una capa sobre un objeto real para espiar las
llamadas que se le hacen
72. External
Event Log
System
Questionnaire
DAO DB
Event Log
API
Questionnaire
Service
No hay resultado
que probar
Añadir unAñadir un
cuestionariocuestionario
Tests de Interacciones: por quéTests de Interacciones: por qué
73. External
Event Log
System
Questionnaire
DAO DB
Event Log
API
Questionnaire
Service
No hay resultado
que probar
Añadir unAñadir un
cuestionariocuestionario
Tests de Interacciones: por quéTests de Interacciones: por qué
¡¡¡NO
LO PROBAM
OS!!!
74. Interacción con MocksInteracción con Mocks
def "add a questionnaire"() {
given: "a questionnaire with two questions"
def q = new Questionnaire()
q.addQuestion(new Question())
q.addQuestion(new Question())
and: "a service with mocked collaborators"
def dao = Mock(QuestionnaireDao)
def eventLog = Mock(EventLogApi)
def service = new QuestionnaireService(dao, eventLog)
when: "the questionnaire is created"
service.addQuestionnaire(q)
then: "the questionnaire + questions are created, the event logged"
1 * dao.addQuestionnaireBean(_)
2 * dao.addQuestionBean(_)
1 * eventLog.registerEvent
{ ev -> ev.type == EventType.ADD_QUESTIONNAIRE }
}
75. Interacción con MocksInteracción con Mocks
def "add a questionnaire"() {
given: "a questionnaire with two questions"
def q = new Questionnaire()
q.addQuestion(new Question())
q.addQuestion(new Question())
and: "a service with mocked collaborators"
def dao = Mock(QuestionnaireDao)
def eventLog = Mock(EventLogApi)
def service = new QuestionnaireService(dao, eventLog)
when: "the questionnaire is created"
service.addQuestionnaire(q)
then: "the questionnaire + questions are created, the event logged"
1 * dao.addQuestionnaireBean(_)
2 * dao.addQuestionBean(_)
1 * eventLog.registerEvent
{ ev -> ev.type == EventType.ADD_QUESTIONNAIRE }
}
76. Interacción con MocksInteracción con Mocks
def "add a questionnaire"() {
given: "a questionnaire with two questions"
def q = new Questionnaire()
q.addQuestion(new Question())
q.addQuestion(new Question())
and: "a service with mocked collaborators"
def dao = Mock(QuestionnaireDao)
def eventLog = Mock(EventLogApi)
def service = new QuestionnaireService(dao, eventLog)
when: "the questionnaire is created"
service.addQuestionnaire(q)
then: "the questionnaire + questions are created, the event logged"
1 * dao.addQuestionnaireBean(_)
2 * dao.addQuestionBean(_)
1 * eventLog.registerEvent
{ ev -> ev.type == EventType.ADD_QUESTIONNAIRE }
}
Interacción = Cardinalidad * Constraint
77. Mocks en SpockMocks en Spock
Cardinalidad
Constraints son iguales que en Stubs
Mocking por defecto lenient (“indulgente”)
Estricto - añadir al final regla: 0 * _
Orden de llamadas no se considera
Para hacerlo, poner cada comprobación en un
bloque “then” diferenciado
1 * subscriber.receive("hello")
0 * subscriber.receive("hello")
(1..3) * subscriber.receive("hello")
(1.._) * subscriber.receive("hello")
(_..3) * subscriber.receive("hello")
_ * subscriber.receive("hello")
78. Shaken, not stirredShaken, not stirred
Interacciones se pueden mezclar con
condiciones de comprobación de datos
Mocks pueden tener métodos stubbeados
Valores por defecto distintos a Stub: 0 / false / null
Spies: wrapper sobre implementación de clase
real
Se pueden chequear interacciones
Se pueden stubbear métodos
79. Shaken, not stirredShaken, not stirred
Interacciones se pueden mezclar con
condiciones de comprobación de datos
Mocks pueden tener métodos stubbeados
Valores por defecto distintos a Stub: 0 / false / null
Spies: wrapper sobre implementación de clase
real
Se pueden chequear interacciones
Se pueden stubbear métodos
81. ¿¿¿Tests a prueba de Vagos???¿¿¿Tests a prueba de Vagos???
Subir nivel de abstracción
No “programar tests” → declarar casos de prueba
Sencillez + potencia
Expresividad → test es a la vez documentación
Fácil de ejecutar en sistemas de integración continua y
en IDEs
Información que facilite la detección de errores
82. ¿¿¿Tests a prueba de Vagos???¿¿¿Tests a prueba de Vagos???
Subir nivel de abstracción
No “programar tests” → declarar casos de prueba
Sencillez + potencia
Expresividad → test es a la vez documentación
Fácil de ejecutar en sistemas de integración continua y
en IDEs
Información que facilite la detección de errores
83. ¿¿¿Tests a prueba de Vagos???¿¿¿Tests a prueba de Vagos???
Subir nivel de abstracción
No “programar tests” → declarar casos de prueba
Sencillez + potencia
Expresividad → test es a la vez documentación
Fácil de ejecutar en sistemas de integración continua y
en IDEs
Información que facilite la detección de errores
84. ¿¿¿Tests a prueba de Vagos???¿¿¿Tests a prueba de Vagos???
Subir nivel de abstracción
No “programar tests” → declarar casos de prueba
Sencillez + potencia
Expresividad → test es a la vez documentación
Fácil de ejecutar en sistemas de integración continua y
en IDEs
Información que facilite la detección de errores
85. ¿¿¿Tests a prueba de Vagos???¿¿¿Tests a prueba de Vagos???
Subir nivel de abstracción
No “programar tests” → declarar casos de prueba
Sencillez + potencia
Expresividad → test es a la vez documentación
Fácil de ejecutar en sistemas de integración continua y
en IDEs
Información que facilite la detección de errores
¡¡¡YESSSSSSSSSSSSS!!!¡¡¡YESSSSSSSSSSSSS!!!
89. Base de datos (objeto Sql)Base de datos (objeto Sql)
@Shared
@AutoCleanup("shutdown")
DataSource ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build();
@Shared
Sql sql = Sql.newInstance(ds)
@Shared
SqlSession session
@Shared
@Subject
QuestionnarieDao dao
def setupSpec() {
// DDL
sql.execute('''
create table questionnaries (
id bigint not null identity,
name varchar(200) not null
);
''')
// MyBatis config / DAO creation
def transactionFactory = new JdbcTransactionFactory();
def environment = new Environment("development", transactionFactory, ds);
def configuration = new Configuration(environment);
configuration.addMapper(QuestionnarieDao.class);
def builder = new SqlSessionFactoryBuilder();
def factory = builder.build(configuration);
session = factory.openSession()
dao = session.getMapper(QuestionnarieDao.class)
}
90. Base de datos (objeto Sql)Base de datos (objeto Sql)
@Shared
@AutoCleanup("shutdown")
DataSource ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build();
@Shared
Sql sql = Sql.newInstance(ds)
@Shared
SqlSession session
@Shared
@Subject
QuestionnarieDao dao
def setupSpec() {
// DDL
sql.execute('''
create table questionnaries (
id bigint not null identity,
name varchar(200) not null
);
''')
// MyBatis config / DAO creation
def transactionFactory = new JdbcTransactionFactory();
def environment = new Environment("development", transactionFactory, ds);
def configuration = new Configuration(environment);
configuration.addMapper(QuestionnarieDao.class);
def builder = new SqlSessionFactoryBuilder();
def factory = builder.build(configuration);
session = factory.openSession()
dao = session.getMapper(QuestionnarieDao.class)
}
91. Base de datos (objeto Sql)Base de datos (objeto Sql)
@Shared
@AutoCleanup("shutdown")
DataSource ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build();
@Shared
Sql sql = Sql.newInstance(ds)
@Shared
SqlSession session
@Shared
@Subject
QuestionnarieDao dao
def setupSpec() {
// DDL
sql.execute('''
create table questionnaries (
id bigint not null identity,
name varchar(200) not null
);
''')
// MyBatis config / DAO creation
def transactionFactory = new JdbcTransactionFactory();
def environment = new Environment("development", transactionFactory, ds);
def configuration = new Configuration(environment);
configuration.addMapper(QuestionnarieDao.class);
def builder = new SqlSessionFactoryBuilder();
def factory = builder.build(configuration);
session = factory.openSession()
dao = session.getMapper(QuestionnarieDao.class)
}
92. Base de datos (objeto Sql)Base de datos (objeto Sql)
def "find questionnaries" () {
final NAME = "Cuestionario de prueba"
given:
sql.execute("insert into questionnaries(name) values (${NAME})")
sql.commit()
when:
def qlist = dao.findActiveQuestionnaries()
then:
qlist.size() == 1
qlist[0].name == NAME
}
def "insert questionnarie" () {
final NAME = "Cuestionario nuevo"
when:
dao.insertQuestionnarie(new Questionnarie([name: NAME]))
session.commit()
then:
sql.firstRow("select * from questionnaries where name = ${NAME}").id != null
and:
sql.rows("select * from questionnaries").size() ==
old(sql.rows("select * from questionnaries").size()) + 1
}
93. Base de datos: DB UnitBase de datos: DB Unit
@Shared
@AutoCleanup("shutdown")
DataSource ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build()
(...)
@DbUnit
def dbState = {
Questionnaries(id: 1, name: 'Cuestionario de prueba')
Questionnaries(id: 2, name: 'Otro cuestionario')
Questionnaries(id: 3, name: 'Y otro más')
}
(...)
def "find questionnaries" () {
when:
def qlist = dao.findActiveQuestionnaries()
then:
qlist.size() == 3
qlist[0].name == "Cuestionario de prueba"
}
94. Base de datos: DB UnitBase de datos: DB Unit
@Shared
@AutoCleanup("shutdown")
DataSource ds = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build()
(...)
@DbUnit
def dbState = {
Questionnaries(id: 1, name: 'Cuestionario de prueba')
Questionnaries(id: 2, name: 'Otro cuestionario')
Questionnaries(id: 3, name: 'Y otro más')
}
(...)
def "find questionnaries" () {
when:
def qlist = dao.findActiveQuestionnaries()
then:
qlist.size() == 3
qlist[0].name == "Cuestionario de prueba"
}
spock-dbunit
107. Pruebas de aceptaciónPruebas de aceptación
@Title("Listado de cuestionarios")
@Narrative(""""
Como creador de juegos de cuestionarios
quiero poder consultar la lista de cuestionarios ya existentes
para poder crear un nuevo cuestionario basado en otro anterior
""")
class QuestionnariesPageSpec extends GebSpec {
def "scenario: comprobación listado"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given: "Estamos en la lista de cuestionarios"
to QuestionnariesListPage
expect: "Que la página sea la correcta"
at QuestionnariesListPage
and: "El número de elementos sea el correcto"
pagination.total == 222
and: "Se comprueba que uno de los elementos sea el correcto"
def quest = questionnaries[5]
quest.description == EXPECTED_ELEMENT
when: "Se clica en él"
quest.link.click()
then: "Se comprueba que se va a su ficha y que el título sea el correcto"
waitFor { at QuestionnariePage }
questionnarieTitle == EXPECTED_ELEMENT
}
}
Historia de usuario
108. Pruebas de aceptaciónPruebas de aceptación
@Title("Listado de cuestionarios")
@Narrative(""""
Como creador de juegos de cuestionarios
quiero poder consultar la lista de cuestionarios ya existentes
para poder crear un nuevo cuestionario basado en otro anterior
""")
class QuestionnariesPageSpec extends GebSpec {
def "scenario: comprobación listado"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given: "Estamos en la lista de cuestionarios"
to QuestionnariesListPage
expect: "Que la página sea la correcta"
at QuestionnariesListPage
and: "El número de elementos sea el correcto"
pagination.total == 222
and: "Se comprueba que uno de los elementos sea el correcto"
def quest = questionnaries[5]
quest.description == EXPECTED_ELEMENT
when: "Se clica en él"
quest.link.click()
then: "Se comprueba que se va a su ficha y que el título sea el correcto"
waitFor { at QuestionnariePage }
questionnarieTitle == EXPECTED_ELEMENT
}
}
Criterios de aceptación
109. Pruebas de aceptaciónPruebas de aceptación
@Title("Listado de cuestionarios")
@Narrative(""""
Como creador de juegos de cuestionarios
quiero poder consultar la lista de cuestionarios ya existentes
para poder crear un nuevo cuestionario basado en otro anterior
""")
class QuestionnariesPageSpec extends GebSpec {
def "scenario: comprobación listado"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given: "Estamos en la lista de cuestionarios"
to QuestionnariesListPage
expect: "Que la página sea la correcta"
at QuestionnariesListPage
and: "El número de elementos sea el correcto"
pagination.total == 222
and: "Se comprueba que uno de los elementos sea el correcto"
def quest = questionnaries[5]
quest.description == EXPECTED_ELEMENT
when: "Se clica en él"
quest.link.click()
then: "Se comprueba que se va a su ficha y que el título sea el correcto"
waitFor { at QuestionnariePage }
questionnarieTitle == EXPECTED_ELEMENT
}
}
Cooperación cliente,
UX, front, back...
110. Pruebas de aceptaciónPruebas de aceptación
@Title("Listado de cuestionarios")
@Narrative(""""
Como creador de juegos de cuestionarios
quiero poder consultar la lista de cuestionarios ya existentes
para poder crear un nuevo cuestionario basado en otro anterior
""")
class QuestionnariesPageSpec extends GebSpec {
def "scenario: comprobación listado"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given: "Estamos en la lista de cuestionarios"
to QuestionnariesListPage
expect: "Que la página sea la correcta"
at QuestionnariesListPage
and: "El número de elementos sea el correcto"
pagination.total == 222
and: "Se comprueba que uno de los elementos sea el correcto"
def quest = questionnaries[5]
quest.description == EXPECTED_ELEMENT
when: "Se clica en él"
quest.link.click()
then: "Se comprueba que se va a su ficha y que el título sea el correcto"
waitFor { at QuestionnariePage }
questionnarieTitle == EXPECTED_ELEMENT
}
}
BDD
Behaviour Driven
Development
Cooperación cliente,
UX, front, back...
111. Más informaciónMás información
Página principal Spock:
http://www.spockframework.org
Documentación:
http://docs.spockframework.org/
Documentación antigua:
http://code.google.com/p/spock/w/list
Spock Web Console
http://meet.spockframework.org/
Proyecto de ejemplo
http://files.spockframework.org/spock-example-0.5-groovy-1.7.zip
Lenguaje Groovy
http://beta.groovy-lang.org/docs/groovy-2.3.1/html/documentation/#_lists
Modificaciones Groovy a librería estándar JDK
http://groovy.codehaus.org/groovy-jdk/