SlideShare uma empresa Scribd logo
1 de 113
Baixar para ler offline
SPOCKSPOCK
Pruebas en Java con Groovy yPruebas en Java con Groovy y
Andrés ViedmaAndrés Viedma
¿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
Pero... ¿de verdad hacemosPero... ¿de verdad hacemos
pruebas?pruebas?
EL CASTIGADOR DE LOS TESTSEL CASTIGADOR DE LOS TESTS
EL CASTIGADOR DE LOS TESTSEL CASTIGADOR DE LOS TESTS
Da su merecido (o sea, pruebas) a
todas las líneas de código
No hace excepciones
ROBIN HOOD, EL INFALIBLEROBIN HOOD, EL INFALIBLE
ROBIN HOOD, EL INFALIBLEROBIN HOOD, EL INFALIBLE
Nunca falla un tiro.
Ni tampoco falla en el código.
Las pruebas son para los torpes
EL INCREIBLE PINOCHOEL INCREIBLE PINOCHO
EL INCREIBLE PINOCHOEL INCREIBLE PINOCHO
Hace muchíiiiisimas pruebas.
No se lo cree ni él.
EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO
EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO
Hacer pruebas es
importante.
EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO
Hacer pruebas es
importante.
Es una pena que
también sea
UN COÑAZO
¿Y TÚ?...¿Y TÚ?...
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
Mirando SPOCKMirando SPOCK
SPOCKSPOCK
Hecho en Groovy
SPOCKSPOCK
Hecho en Groovy
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
¡Uf! Para montar eso
voy a necesitar...
¡Uf! Para montar eso
voy a necesitar...
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerId>groovy-eclipse-compiler</compilerId>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-compiler</artifactId>
<version>2.8.0-01</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-batch</artifactId>
<version>2.1.8-01</version>
</dependency>
</dependencies>
</plugin>
1. Compilar código Groovy1. Compilar código Groovy
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerId>groovy-eclipse-compiler</compilerId>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-compiler</artifactId>
<version>2.8.0-01</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-batch</artifactId>
<version>2.1.8-01</version>
</dependency>
</dependencies>
</plugin>
1. Compilar código Groovy1. Compilar código Groovy
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerId>groovy-eclipse-compiler</compilerId>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-compiler</artifactId>
<version>2.8.0-01</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-eclipse-batch</artifactId>
<version>2.1.8-01</version>
</dependency>
</dependencies>
</plugin>
1. Compilar código Groovy1. Compilar código Groovy
2. Dependencias con Spock2. Dependencias con Spock
<!-- Test dependencies -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>0.7-groovy-2.0</version>
<scope>test</scope>
</dependency>
3. Ejecutar tests *Spec (opc.)3. Ejecutar tests *Spec (opc.)
<!-- Surefire: include Spock tests (*Spec) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.14</version>
<configuration>
<includes>
<include>**/*Spec.java</include>
<include>**/Test*.java</include>
<include>**/*Test.java</include>
<include>**/*TestCase.java</include>
</includes>
</configuration>
</plugin>
3. Ejecutar tests *Spec (opc.)3. Ejecutar tests *Spec (opc.)
<!-- Surefire: include Spock tests (*Spec) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.14</version>
<configuration>
<includes>
<include>**/*Spec.java</include>
<include>**/Test*.java</include>
<include>**/*Test.java</include>
<include>**/*TestCase.java</include>
</includes>
</configuration>
</plugin>
Sólo dependenciasSólo dependencias
apply plugin: 'groovy'
// spock
testCompile 'org.codehaus.groovy:groovy-all:2.1.5'
testCompile(
group:'org.spockframework',name:'spock-core',
version:'0.7-groovy-2.0')
¿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
¿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
Mi primer test SPOCKMi primer test SPOCK
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
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
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
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"]
}
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"]
}
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"]
}
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
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
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
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
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
Ejecutando...Ejecutando...
Ejecutando...Ejecutando...
Ejecutando...Ejecutando...
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"]
}
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"]
}
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
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
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
}
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
}
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
Matchers HamcrestMatchers Hamcrest
import static spock.util.matcher.HamcrestMatchers.closeTo
class HamcrestMatchers extends Specification {
def "comparing two decimal numbers"() {
def myPi = 3.14
expect:
myPi closeTo(Math.PI, 0.01)
}
}
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
Tests basados enTests basados en
DatosDatos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
s1 | s2 | descrip || res
"pepito" | "pepito" | "same values" || 0
"pepito" | "pePito" | "only case difference" || 1
"pepito" | "qerida" | "many char differences" || 4
"pepito" | "p" | "shorter value" || 5
"p" | "otro" | "larger value" || 4
"12345" | "6" | "all different" || 5
"" | "1234" | "empty and non empty" || 4
"" | "" | "both empty" || 0
"12 34" | "12 34" | "differences in spaces" || 1
"one vision"| "one visn" | "two chars in the middle" || 2
}
Tablas de datosTablas de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
s1 | s2 | descrip || res
"pepito" | "pepito" | "same values" || 0
"pepito" | "pePito" | "only case difference" || 1
"pepito" | "qerida" | "many char differences" || 4
"pepito" | "p" | "shorter value" || 5
"p" | "otro" | "larger value" || 4
"12345" | "6" | "all different" || 5
"" | "1234" | "empty and non empty" || 4
"" | "" | "both empty" || 0
"12 34" | "12 34" | "differences in spaces" || 1
"one vision"| "one visn" | "two chars in the middle" || 2
}
Tablas de datosTablas de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
s1 | s2 | descrip || res
"pepito" | "pepito" | "same values" || 0
"pepito" | "pePito" | "only case difference" || 1
"pepito" | "qerida" | "many char differences" || 4
"pepito" | "p" | "shorter value" || 5
"p" | "otro" | "larger value" || 4
"12345" | "6" | "all different" || 5
"" | "1234" | "empty and non empty" || 4
"" | "" | "both empty" || 0
"12 34" | "12 34" | "differences in spaces" || 1
"one vision"| "one visn" | "two chars in the middle" || 2
}
Tablas de datosTablas de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
s1 | s2 | descrip || res
"pepito" | "pepito" | "same values" || 0
"pepito" | "pePito" | "only case difference" || 1
"pepito" | "qerida" | "many char differences" || 4
"pepito" | "p" | "shorter value" || 5
"p" | "otro" | "larger value" || 4
"12345" | "6" | "all different" || 5
"" | "1234" | "empty and non empty" || 4
"" | "" | "both empty" || 0
"12 34" | "12 34" | "differences in spaces" || 1
"one vision"| "one visn" | "two chars in the middle" || 2
}
Tablas de datosTablas de datos
Tests diferenciados
Pipes de datosPipes de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
s1 << ["pepito", "pepito", "pepito", "pepito", "p"]
s2 << ["pepito", "pePito", "qerida", "p", "otro"]
descrip << ["same values", "only case difference",
"many char differences", "shorter value",
"larger value"]
res << [0, 1, 4, 5, 4]
}
Pipes de datosPipes de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
s1 << ["pepito", "pepito", "pepito", "pepito", "p"]
s2 << ["pepito", "pePito", "qerida", "p", "otro"]
descrip << ["same values", "only case difference",
"many char differences", "shorter value",
"larger value"]
res << [0, 1, 4, 5, 4]
}
Pipes de datosPipes de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
[s1, s2, descrip, resStr] <<
new File("testdata.csv").readLines()
.collect {line -> line.tokenize(",")}
res = Integer.parseInt(resStr)
}
Pipes de datosPipes de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
[s1, s2, descrip, resStr] <<
new File("testdata.csv").readLines()
.collect {line -> line.tokenize(",")}
res = Integer.parseInt(resStr)
}
Pipes de datosPipes de datos
@Unroll
def "distance on #descrip"() {
expect:
LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res
(res == 0? s1 == s2 : s1 != s2)
where:
[s1, s2, descrip, resStr] <<
new File("testdata.csv").readLines()
.collect {line -> line.tokenize(",")}
res = Integer.parseInt(resStr)
}
Asignaciones de variables
““Test doubles”Test doubles”
(mock objects)(mock objects)
¿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
StubsStubs
def "check valid comics"() {
given:
def apiStub = Stub(MarvelApi) {
findComicsByCharacter(_) >> [
new MarvelComic(id: 1, date: new Date(),
creators: [new ComicCreator(id: 101)]
),
(............)
new MarvelComic(id: 6, date: null,
creators: [new ComicCreator(id: 103)]
)
]
}
MarvelQuestionnaireFactory f = new MarvelQuestionFactory(apiStub)
expect:
f.loadValidQuestionnarieComics(1)*.id == [1, 5]
}
StubsStubs
def "check valid comics"() {
given:
def apiStub = Stub(MarvelApi) {
findComicsByCharacter(_) >> [
new MarvelComic(id: 1, date: new Date(),
creators: [new ComicCreator(id: 101)]
),
(............)
new MarvelComic(id: 6, date: null,
creators: [new ComicCreator(id: 103)]
)
]
}
MarvelQuestionnaireFactory f = new MarvelQuestionFactory(apiStub)
expect:
f.loadValidQuestionnarieComics(1)*.id == [1, 5]
}
Stub de una clase
añadir dependencias a
cglib-nodep y objenesis
StubsStubs
def "check valid comics"() {
given:
def apiStub = Stub(MarvelApi) {
findComicsByCharacter(_) >> [
new MarvelComic(id: 1, date: new Date(),
creators: [new ComicCreator(id: 101)]
),
(............)
new MarvelComic(id: 6, date: null,
creators: [new ComicCreator(id: 103)]
)
]
}
MarvelQuestionnaireFactory f = new MarvelQuestionFactory(apiStub)
expect:
f.loadValidQuestionnarieComics(1)*.id == [1, 5]
}
Named parameter constructor
Stubs: constraintsStubs: constraints
Método: admite expresiones regulares
api./findComicsBy.*/(...)
Propiedad (getter)
api.apiKey
Argumentos
stub.metodo("hello")
stub.metodo(!"hello")
stub.metodo()
stub.metodo(_)
stub.metodo(*_)
stub.metodo(_ as String)
stub.metodo({ l -> l.size() > 3 })
Stubs: comportamientoStubs: comportamiento
Siempre devolver mismo valor (>>)
stub.metodo(args) >> result1
Devolver valores secuencialmente (>>>)
stub.metodo(args) >>> [res1, res2, res3]
Ejecución de código (cambio estado, calcular retorno)
stub.metodo(...) >> { args -> ..... }
stub.metodo(...) >> { arg -> ..... }
Encadenar llamadas de distinto tipo
stub.metodo(args) >>> [r1, r2] >> { (code) }
>> r4
Llamada no declarada: valor por defecto / objeto vacío (no null)
Tests basados enTests basados en
InteraccionesInteracciones
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é
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!!!
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 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 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
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")
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
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
Recapitulemos...Recapitulemos...
¿¿¿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
¿¿¿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
¿¿¿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
¿¿¿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
¿¿¿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!!!
¿Ibas a alguna parte?...
¡¡¡¿¿¿SOMOS HOMBRES¡¡¡¿¿¿SOMOS HOMBRES
O NENAZAS???!!!O NENAZAS???!!!
¿Ibas a alguna parte?...
Tests de integraciónTests de integración
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)
}
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)
}
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)
}
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
}
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"
}
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
SpringSpring
@ContextConfiguration(locations = "classpath:spring/application-config.xml")
class CourseRestControllerSpec extends Specification {
@Autowired
@Subject
CourseRestController controller
def "get courses"() {
when:
ListPage<Course> courses = controller.getCourses(new PaginationDesc(from: 1, max: 10))
then:
courses.listSize == 7
courses.elements[2].title == "Intensivo de rueda cubana"
}
}
SpringSpring
@ContextConfiguration(locations = "classpath:spring/application-config.xml")
class CourseRestControllerSpec extends Specification {
@Autowired
@Subject
CourseRestController controller
def "get courses"() {
when:
ListPage<Course> courses = controller.getCourses(new PaginationDesc(from: 1, max: 10))
then:
courses.listSize == 7
courses.elements[2].title == "Intensivo de rueda cubana"
}
}
spock-spring
TestsTests
FuncionalesFuncionales
(web)(web)
Tests web funcionalesTests web funcionales
class QuestionnariesPageSpec extends GebSpec {
def "questionnaries page check"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given:
go "/es/questionnaries"
expect:
$("p.recordcount > .valor").text() == "7"
and:
def link = $("ol.pag-registros > li .media-heading a")[5]
link.text() == EXPECTED_ELEMENT
when:
link.click()
then:
title == EXPECTED_ELEMENT
}
}
Tests web funcionalesTests web funcionales
class QuestionnariesPageSpec extends GebSpec {
def "questionnaries page check"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given:
go "/es/questionnaries"
expect:
$("p.recordcount > .valor").text() == "7"
and:
def link = $("ol.pag-registros > li .media-heading a")[5]
link.text() == EXPECTED_ELEMENT
when:
link.click()
then:
title == EXPECTED_ELEMENT
}
}
GebGeb
Very Groovy Browser Automation
Basado en Selenium
http://www.gebish.org/
Permite hacer capturas (reporting)
<dependency>
<groupId>org.gebish</groupId>
<artifactId>geb-spock</artifactId>
<version>0.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-htmlunit-driver</artifactId>
<version>2.26.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-firefox-driver</artifactId>
<version>2.26.0</version>
<scope>test</scope>
</dependency>
Dependencias
Configuracioń:
/GebConfig.groovy (DSL)
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
driver = { new HtmlUnitDriver() }
baseUrl = "http://xxxxxxxxxxxxxxxxx"
Instalar Drivers
(PhantomJS, Firefox...)
Geb con Page ObjectsGeb con Page Objects
class PaginationModule extends Module {
def root
static content = {
paginationbar { root.find(".paginationbar") }
total { paginationbar.find (".recordcount .valor").text() as int }
pageElements { root.find("ol.pag-registros > li") }
}
}
class QuestionnarieHeader extends Module {
def root
static content = {
link { root.find(".media-heading a") }
description { link.text() }
}
}
class QuestionnariesListPage extends Page {
static url = "/es/questionnaries"
static at = { title == "Registros" }
static content = {
pagination { module PaginationModule,
root: $(".sumario_registros .pagination-container") }
questionnaries { pagination.pageElements.collect
{ module QuestionnarieHeader, root: it } }
}
}
Geb con Page ObjectsGeb con Page Objects
class PaginationModule extends Module {
def root
static content = {
paginationbar { root.find(".paginationbar") }
total { paginationbar.find (".recordcount .valor").text() as int }
pageElements { root.find("ol.pag-registros > li") }
}
}
class QuestionnarieHeader extends Module {
def root
static content = {
link { root.find(".media-heading a") }
description { link.text() }
}
}
class QuestionnariesListPage extends Page {
static url = "/es/questionnaries"
static at = { title == "Registros" }
static content = {
pagination { module PaginationModule,
root: $(".sumario_registros .pagination-container") }
questionnaries { pagination.pageElements.collect
{ module QuestionnarieHeader, root: it } }
}
}
Page object
- url: para ir a la página
- at: para comprobar si estamos
en ella
- content: acceso rápido a
elementos
Geb con Page ObjectsGeb con Page Objects
class PaginationModule extends Module {
def root
static content = {
paginationbar { root.find(".paginationbar") }
total { paginationbar.find (".recordcount .valor").text() as int }
pageElements { root.find("ol.pag-registros > li") }
}
}
class QuestionnarieHeader extends Module {
def root
static content = {
link { root.find(".media-heading a") }
description { link.text() }
}
}
class QuestionnariesListPage extends Page {
static url = "/es/questionnaries"
static at = { title == "Registros" }
static content = {
pagination { module PaginationModule,
root: $(".sumario_registros .pagination-container") }
questionnaries { pagination.pageElements.collect
{ module QuestionnarieHeader, root: it } }
}
}
Module object
Elemento reutilizable por
varias páginas
Geb con Page ObjectsGeb con Page Objects
class PaginationModule extends Module {
def root
static content = {
paginationbar { root.find(".paginationbar") }
total { paginationbar.find (".recordcount .valor").text() as int }
pageElements { root.find("ol.pag-registros > li") }
}
}
class QuestionnarieHeader extends Module {
def root
static content = {
link { root.find(".media-heading a") }
description { link.text() }
}
}
class QuestionnariesListPage extends Page {
static url = "/es/questionnaries"
static at = { title == "Registros" }
static content = {
pagination { module PaginationModule,
root: $(".sumario_registros .pagination-container") }
questionnaries { pagination.pageElements.collect
{ module QuestionnarieHeader, root: it } }
}
}
Forma de usar los módulos
dentro de un page object
Geb con Page ObjectsGeb con Page Objects
class QuestionnariesPageSpec extends GebSpec {
def "questionnaries page check"() {
final EXPECTED_ELEMENT = "Cuestionario chulo"
given:
to QuestionnariesListPage
expect:
at QuestionnariesListPage
and:
pagination.total == 7
and:
def quest = questionnaries[5]
quest.description == EXPECTED_ELEMENT
when:
quest.link.click()
then:
waitFor { at QuestionnariePage }
questionnarieTitle == EXPECTED_ELEMENT
}
}
Tests deTests de
AceptaciónAceptación
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
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
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...
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...
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/
Más informaciónMás información
Geb
http://www.gebish.org/
spock-spring
http://code.google.com/p/spock/wiki/SpringExtension
spock-dbunit
https://github.com/janbols/spock-dbunit
Gracias por la atención...Gracias por la atención...
Andrés ViedmaAndrés Viedma
@andres_viedma@andres_viedma

Mais conteúdo relacionado

Mais procurados

Beyond Java: 자바 8을 중심으로 본 자바의 혁신
Beyond Java: 자바 8을 중심으로 본 자바의 혁신Beyond Java: 자바 8을 중심으로 본 자바의 혁신
Beyond Java: 자바 8을 중심으로 본 자바의 혁신Sungchul Park
 
Camel JBang - Quarkus Insights.pdf
Camel JBang - Quarkus Insights.pdfCamel JBang - Quarkus Insights.pdf
Camel JBang - Quarkus Insights.pdfClaus Ibsen
 
Tree data structure in java
Tree data structure in javaTree data structure in java
Tree data structure in javaIrfan CH
 
Lab manual data structure (cs305 rgpv) (usefulsearch.org) (useful search)
Lab manual data structure (cs305 rgpv) (usefulsearch.org)  (useful search)Lab manual data structure (cs305 rgpv) (usefulsearch.org)  (useful search)
Lab manual data structure (cs305 rgpv) (usefulsearch.org) (useful search)Make Mannan
 
Kotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is comingKotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is comingKirill Rozov
 
Testes pythonicos com pytest
Testes pythonicos com pytestTestes pythonicos com pytest
Testes pythonicos com pytestviniciusban
 
FLOW OF CONTROL-INTRO PYTHON
FLOW OF CONTROL-INTRO PYTHONFLOW OF CONTROL-INTRO PYTHON
FLOW OF CONTROL-INTRO PYTHONvikram mahendra
 
Modern Programming in Java 8 - Lambdas, Streams and Date Time API
Modern Programming in Java 8 - Lambdas, Streams and Date Time APIModern Programming in Java 8 - Lambdas, Streams and Date Time API
Modern Programming in Java 8 - Lambdas, Streams and Date Time APIGanesh Samarthyam
 
Introduction to Python - Part Two
Introduction to Python - Part TwoIntroduction to Python - Part Two
Introduction to Python - Part Twoamiable_indian
 
Python Visual Studio | Edureka
Python Visual Studio | EdurekaPython Visual Studio | Edureka
Python Visual Studio | EdurekaEdureka!
 
A journey beyond the page object pattern
A journey beyond the page object patternA journey beyond the page object pattern
A journey beyond the page object patternRiverGlide
 
Python Course | Python Programming | Python Tutorial | Python Training | Edureka
Python Course | Python Programming | Python Tutorial | Python Training | EdurekaPython Course | Python Programming | Python Tutorial | Python Training | Edureka
Python Course | Python Programming | Python Tutorial | Python Training | EdurekaEdureka!
 
The Future of Java: Records, Sealed Classes and Pattern Matching
The Future of Java: Records, Sealed Classes and Pattern MatchingThe Future of Java: Records, Sealed Classes and Pattern Matching
The Future of Java: Records, Sealed Classes and Pattern MatchingJosé Paumard
 
Async await in C++
Async await in C++Async await in C++
Async await in C++cppfrug
 
Introduction to the basics of Python programming (part 1)
Introduction to the basics of Python programming (part 1)Introduction to the basics of Python programming (part 1)
Introduction to the basics of Python programming (part 1)Pedro Rodrigues
 
ROS 2 Embedded WG SPRESENSE RDC
ROS 2 Embedded WG SPRESENSE RDCROS 2 Embedded WG SPRESENSE RDC
ROS 2 Embedded WG SPRESENSE RDCTomoya Fujita
 

Mais procurados (20)

Beyond Java: 자바 8을 중심으로 본 자바의 혁신
Beyond Java: 자바 8을 중심으로 본 자바의 혁신Beyond Java: 자바 8을 중심으로 본 자바의 혁신
Beyond Java: 자바 8을 중심으로 본 자바의 혁신
 
Camel JBang - Quarkus Insights.pdf
Camel JBang - Quarkus Insights.pdfCamel JBang - Quarkus Insights.pdf
Camel JBang - Quarkus Insights.pdf
 
Tree data structure in java
Tree data structure in javaTree data structure in java
Tree data structure in java
 
Lab manual data structure (cs305 rgpv) (usefulsearch.org) (useful search)
Lab manual data structure (cs305 rgpv) (usefulsearch.org)  (useful search)Lab manual data structure (cs305 rgpv) (usefulsearch.org)  (useful search)
Lab manual data structure (cs305 rgpv) (usefulsearch.org) (useful search)
 
An Introduction to Python Concurrency
An Introduction to Python ConcurrencyAn Introduction to Python Concurrency
An Introduction to Python Concurrency
 
Kotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is comingKotlin Coroutines. Flow is coming
Kotlin Coroutines. Flow is coming
 
Testes pythonicos com pytest
Testes pythonicos com pytestTestes pythonicos com pytest
Testes pythonicos com pytest
 
FLOW OF CONTROL-INTRO PYTHON
FLOW OF CONTROL-INTRO PYTHONFLOW OF CONTROL-INTRO PYTHON
FLOW OF CONTROL-INTRO PYTHON
 
Modern Programming in Java 8 - Lambdas, Streams and Date Time API
Modern Programming in Java 8 - Lambdas, Streams and Date Time APIModern Programming in Java 8 - Lambdas, Streams and Date Time API
Modern Programming in Java 8 - Lambdas, Streams and Date Time API
 
Introduction to Python - Part Two
Introduction to Python - Part TwoIntroduction to Python - Part Two
Introduction to Python - Part Two
 
Python Visual Studio | Edureka
Python Visual Studio | EdurekaPython Visual Studio | Edureka
Python Visual Studio | Edureka
 
A journey beyond the page object pattern
A journey beyond the page object patternA journey beyond the page object pattern
A journey beyond the page object pattern
 
Python Course | Python Programming | Python Tutorial | Python Training | Edureka
Python Course | Python Programming | Python Tutorial | Python Training | EdurekaPython Course | Python Programming | Python Tutorial | Python Training | Edureka
Python Course | Python Programming | Python Tutorial | Python Training | Edureka
 
PythonOOP
PythonOOPPythonOOP
PythonOOP
 
The Future of Java: Records, Sealed Classes and Pattern Matching
The Future of Java: Records, Sealed Classes and Pattern MatchingThe Future of Java: Records, Sealed Classes and Pattern Matching
The Future of Java: Records, Sealed Classes and Pattern Matching
 
Async await in C++
Async await in C++Async await in C++
Async await in C++
 
Introduction to the basics of Python programming (part 1)
Introduction to the basics of Python programming (part 1)Introduction to the basics of Python programming (part 1)
Introduction to the basics of Python programming (part 1)
 
Begin with Python
Begin with PythonBegin with Python
Begin with Python
 
Python sqlite3
Python sqlite3Python sqlite3
Python sqlite3
 
ROS 2 Embedded WG SPRESENSE RDC
ROS 2 Embedded WG SPRESENSE RDCROS 2 Embedded WG SPRESENSE RDC
ROS 2 Embedded WG SPRESENSE RDC
 

Semelhante a Tests en Java con Groovy y Spock

Introducción a Scala
Introducción a ScalaIntroducción a Scala
Introducción a Scaladhaat
 
Write gradle plugins escribir y publicar tus plugins de gradle made easy_
Write gradle plugins  escribir y publicar tus plugins de gradle  made easy_Write gradle plugins  escribir y publicar tus plugins de gradle  made easy_
Write gradle plugins escribir y publicar tus plugins de gradle made easy_Jorge Aguilera
 
Groovy no es java sin punto y coma v3
Groovy no es java sin punto y coma v3Groovy no es java sin punto y coma v3
Groovy no es java sin punto y coma v3Pablo Alba
 
Groovy no es java sin puntos y comas - Codemotion Madrid 2014
Groovy no es java sin puntos y comas - Codemotion Madrid 2014Groovy no es java sin puntos y comas - Codemotion Madrid 2014
Groovy no es java sin puntos y comas - Codemotion Madrid 2014Pablo Alba
 
Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)
Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)
Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)Agustin Ramos
 
Jruby On Rails. Ruby on Rails en la JVM
Jruby On Rails. Ruby on Rails en la JVMJruby On Rails. Ruby on Rails en la JVM
Jruby On Rails. Ruby on Rails en la JVMjavier ramirez
 
Manual de android
Manual de androidManual de android
Manual de androidJarboledah
 
Dart como alternativa a TypeScript (Codemotion 2016)
Dart como alternativa a TypeScript (Codemotion 2016)Dart como alternativa a TypeScript (Codemotion 2016)
Dart como alternativa a TypeScript (Codemotion 2016)Rafael Bermúdez Míguez
 
Ocho cosas que debes saber de JavaScript
Ocho cosas que debes saber de JavaScriptOcho cosas que debes saber de JavaScript
Ocho cosas que debes saber de JavaScriptDavid Ballén
 
Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...
Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...
Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...TestingUy
 
Property Based Testing usando QuickCheck
Property Based Testing usando QuickCheckProperty Based Testing usando QuickCheck
Property Based Testing usando QuickCheckguillecabeza
 
Tu api ha muerto larga vida a tu dsl
Tu api ha muerto  larga vida a tu dslTu api ha muerto  larga vida a tu dsl
Tu api ha muerto larga vida a tu dslJorge Aguilera
 

Semelhante a Tests en Java con Groovy y Spock (20)

Write Gradle Plugins
Write Gradle PluginsWrite Gradle Plugins
Write Gradle Plugins
 
Introducción a Scala
Introducción a ScalaIntroducción a Scala
Introducción a Scala
 
Write gradle plugins escribir y publicar tus plugins de gradle made easy_
Write gradle plugins  escribir y publicar tus plugins de gradle  made easy_Write gradle plugins  escribir y publicar tus plugins de gradle  made easy_
Write gradle plugins escribir y publicar tus plugins de gradle made easy_
 
Groovy no es java sin punto y coma v3
Groovy no es java sin punto y coma v3Groovy no es java sin punto y coma v3
Groovy no es java sin punto y coma v3
 
Groovy no es java sin puntos y comas - Codemotion Madrid 2014
Groovy no es java sin puntos y comas - Codemotion Madrid 2014Groovy no es java sin puntos y comas - Codemotion Madrid 2014
Groovy no es java sin puntos y comas - Codemotion Madrid 2014
 
Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)
Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)
Desarrollo Dirigido por Comportamiento (con Cucumber y Groovy)
 
Introducción a Groovy
Introducción a GroovyIntroducción a Groovy
Introducción a Groovy
 
Jruby On Rails. Ruby on Rails en la JVM
Jruby On Rails. Ruby on Rails en la JVMJruby On Rails. Ruby on Rails en la JVM
Jruby On Rails. Ruby on Rails en la JVM
 
Curso android studio
Curso android studioCurso android studio
Curso android studio
 
Curso android studio
Curso android studioCurso android studio
Curso android studio
 
Manual de android
Manual de androidManual de android
Manual de android
 
Programación de Aplicaciones
Programación de AplicacionesProgramación de Aplicaciones
Programación de Aplicaciones
 
Dart como alternativa a TypeScript (Codemotion 2016)
Dart como alternativa a TypeScript (Codemotion 2016)Dart como alternativa a TypeScript (Codemotion 2016)
Dart como alternativa a TypeScript (Codemotion 2016)
 
Ocho cosas que debes saber de JavaScript
Ocho cosas que debes saber de JavaScriptOcho cosas que debes saber de JavaScript
Ocho cosas que debes saber de JavaScript
 
Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...
Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...
Charla evento TestingUY 2015 - Property-Based Testing Usando Quickcheck, o có...
 
Property Based Testing usando QuickCheck
Property Based Testing usando QuickCheckProperty Based Testing usando QuickCheck
Property Based Testing usando QuickCheck
 
Introducción a Clojure
Introducción a ClojureIntroducción a Clojure
Introducción a Clojure
 
Tu api ha muerto larga vida a tu dsl
Tu api ha muerto  larga vida a tu dslTu api ha muerto  larga vida a tu dsl
Tu api ha muerto larga vida a tu dsl
 
Clase 7 objetos globales de javaScript
Clase 7 objetos globales de javaScriptClase 7 objetos globales de javaScript
Clase 7 objetos globales de javaScript
 
Personalizar gui guia_3
Personalizar gui guia_3Personalizar gui guia_3
Personalizar gui guia_3
 

Último

Evaluación del riesgo tecnologías informáticas.pdf
Evaluación del riesgo tecnologías informáticas.pdfEvaluación del riesgo tecnologías informáticas.pdf
Evaluación del riesgo tecnologías informáticas.pdfGuillermoBarquero7
 
Caso de éxito de Hervian con el ERP Sage 200
Caso de éxito de Hervian con el ERP Sage 200Caso de éxito de Hervian con el ERP Sage 200
Caso de éxito de Hervian con el ERP Sage 200Opentix
 
Caso de Exito LPL Projects Logistics Spain y Business Central
Caso de Exito LPL Projects Logistics Spain y Business CentralCaso de Exito LPL Projects Logistics Spain y Business Central
Caso de Exito LPL Projects Logistics Spain y Business CentralAitana
 
2da. Clase Mecanografía e introducción a Excel (2).pptx
2da. Clase Mecanografía e introducción a Excel (2).pptx2da. Clase Mecanografía e introducción a Excel (2).pptx
2da. Clase Mecanografía e introducción a Excel (2).pptxEncomiendasElSherpa
 
ESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOS
ESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOSESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOS
ESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOSBeatrizGonzales19
 
Trabajo de Powerpoint - Unsaac - Ofimática
Trabajo de Powerpoint - Unsaac - OfimáticaTrabajo de Powerpoint - Unsaac - Ofimática
Trabajo de Powerpoint - Unsaac - OfimáticaKANTUPAULAPORCELYUCR
 

Último (6)

Evaluación del riesgo tecnologías informáticas.pdf
Evaluación del riesgo tecnologías informáticas.pdfEvaluación del riesgo tecnologías informáticas.pdf
Evaluación del riesgo tecnologías informáticas.pdf
 
Caso de éxito de Hervian con el ERP Sage 200
Caso de éxito de Hervian con el ERP Sage 200Caso de éxito de Hervian con el ERP Sage 200
Caso de éxito de Hervian con el ERP Sage 200
 
Caso de Exito LPL Projects Logistics Spain y Business Central
Caso de Exito LPL Projects Logistics Spain y Business CentralCaso de Exito LPL Projects Logistics Spain y Business Central
Caso de Exito LPL Projects Logistics Spain y Business Central
 
2da. Clase Mecanografía e introducción a Excel (2).pptx
2da. Clase Mecanografía e introducción a Excel (2).pptx2da. Clase Mecanografía e introducción a Excel (2).pptx
2da. Clase Mecanografía e introducción a Excel (2).pptx
 
ESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOS
ESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOSESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOS
ESCRITORIO DE WINDOWS 11 Y SUS ELEMENTOS
 
Trabajo de Powerpoint - Unsaac - Ofimática
Trabajo de Powerpoint - Unsaac - OfimáticaTrabajo de Powerpoint - Unsaac - Ofimática
Trabajo de Powerpoint - Unsaac - Ofimática
 

Tests en Java con Groovy y Spock

  • 1. SPOCKSPOCK Pruebas en Java con Groovy yPruebas en Java con Groovy y Andrés ViedmaAndrés Viedma
  • 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
  • 3. Pero... ¿de verdad hacemosPero... ¿de verdad hacemos pruebas?pruebas?
  • 4. EL CASTIGADOR DE LOS TESTSEL CASTIGADOR DE LOS TESTS
  • 5. EL CASTIGADOR DE LOS TESTSEL CASTIGADOR DE LOS TESTS Da su merecido (o sea, pruebas) a todas las líneas de código No hace excepciones
  • 6. ROBIN HOOD, EL INFALIBLEROBIN HOOD, EL INFALIBLE
  • 7. ROBIN HOOD, EL INFALIBLEROBIN HOOD, EL INFALIBLE Nunca falla un tiro. Ni tampoco falla en el código. Las pruebas son para los torpes
  • 8. EL INCREIBLE PINOCHOEL INCREIBLE PINOCHO
  • 9. EL INCREIBLE PINOCHOEL INCREIBLE PINOCHO Hace muchíiiiisimas pruebas. No se lo cree ni él.
  • 10. EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO
  • 11. EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO Hacer pruebas es importante.
  • 12. EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO Hacer pruebas es importante. Es una pena que también sea UN COÑAZO
  • 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
  • 19. ¡Uf! Para montar eso voy a necesitar...
  • 20. ¡Uf! Para montar eso voy a necesitar...
  • 24. 2. Dependencias con Spock2. Dependencias con Spock <!-- Test dependencies --> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.1.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>0.7-groovy-2.0</version> <scope>test</scope> </dependency>
  • 25. 3. Ejecutar tests *Spec (opc.)3. Ejecutar tests *Spec (opc.) <!-- Surefire: include Spock tests (*Spec) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.14</version> <configuration> <includes> <include>**/*Spec.java</include> <include>**/Test*.java</include> <include>**/*Test.java</include> <include>**/*TestCase.java</include> </includes> </configuration> </plugin>
  • 26. 3. Ejecutar tests *Spec (opc.)3. Ejecutar tests *Spec (opc.) <!-- Surefire: include Spock tests (*Spec) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.14</version> <configuration> <includes> <include>**/*Spec.java</include> <include>**/Test*.java</include> <include>**/*Test.java</include> <include>**/*TestCase.java</include> </includes> </configuration> </plugin>
  • 27. Sólo dependenciasSólo dependencias apply plugin: 'groovy' // spock testCompile 'org.codehaus.groovy:groovy-all:2.1.5' testCompile( group:'org.spockframework',name:'spock-core', version:'0.7-groovy-2.0')
  • 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
  • 30. Mi primer test SPOCKMi primer test SPOCK
  • 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
  • 52. Matchers HamcrestMatchers Hamcrest import static spock.util.matcher.HamcrestMatchers.closeTo class HamcrestMatchers extends Specification { def "comparing two decimal numbers"() { def myPi = 3.14 expect: myPi closeTo(Math.PI, 0.01) } }
  • 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
  • 54. Tests basados enTests basados en DatosDatos
  • 55. @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: s1 | s2 | descrip || res "pepito" | "pepito" | "same values" || 0 "pepito" | "pePito" | "only case difference" || 1 "pepito" | "qerida" | "many char differences" || 4 "pepito" | "p" | "shorter value" || 5 "p" | "otro" | "larger value" || 4 "12345" | "6" | "all different" || 5 "" | "1234" | "empty and non empty" || 4 "" | "" | "both empty" || 0 "12 34" | "12 34" | "differences in spaces" || 1 "one vision"| "one visn" | "two chars in the middle" || 2 } Tablas de datosTablas de datos
  • 56. @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: s1 | s2 | descrip || res "pepito" | "pepito" | "same values" || 0 "pepito" | "pePito" | "only case difference" || 1 "pepito" | "qerida" | "many char differences" || 4 "pepito" | "p" | "shorter value" || 5 "p" | "otro" | "larger value" || 4 "12345" | "6" | "all different" || 5 "" | "1234" | "empty and non empty" || 4 "" | "" | "both empty" || 0 "12 34" | "12 34" | "differences in spaces" || 1 "one vision"| "one visn" | "two chars in the middle" || 2 } Tablas de datosTablas de datos
  • 57. @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: s1 | s2 | descrip || res "pepito" | "pepito" | "same values" || 0 "pepito" | "pePito" | "only case difference" || 1 "pepito" | "qerida" | "many char differences" || 4 "pepito" | "p" | "shorter value" || 5 "p" | "otro" | "larger value" || 4 "12345" | "6" | "all different" || 5 "" | "1234" | "empty and non empty" || 4 "" | "" | "both empty" || 0 "12 34" | "12 34" | "differences in spaces" || 1 "one vision"| "one visn" | "two chars in the middle" || 2 } Tablas de datosTablas de datos
  • 58. @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: s1 | s2 | descrip || res "pepito" | "pepito" | "same values" || 0 "pepito" | "pePito" | "only case difference" || 1 "pepito" | "qerida" | "many char differences" || 4 "pepito" | "p" | "shorter value" || 5 "p" | "otro" | "larger value" || 4 "12345" | "6" | "all different" || 5 "" | "1234" | "empty and non empty" || 4 "" | "" | "both empty" || 0 "12 34" | "12 34" | "differences in spaces" || 1 "one vision"| "one visn" | "two chars in the middle" || 2 } Tablas de datosTablas de datos Tests diferenciados
  • 59. Pipes de datosPipes de datos @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: s1 << ["pepito", "pepito", "pepito", "pepito", "p"] s2 << ["pepito", "pePito", "qerida", "p", "otro"] descrip << ["same values", "only case difference", "many char differences", "shorter value", "larger value"] res << [0, 1, 4, 5, 4] }
  • 60. Pipes de datosPipes de datos @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: s1 << ["pepito", "pepito", "pepito", "pepito", "p"] s2 << ["pepito", "pePito", "qerida", "p", "otro"] descrip << ["same values", "only case difference", "many char differences", "shorter value", "larger value"] res << [0, 1, 4, 5, 4] }
  • 61. Pipes de datosPipes de datos @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: [s1, s2, descrip, resStr] << new File("testdata.csv").readLines() .collect {line -> line.tokenize(",")} res = Integer.parseInt(resStr) }
  • 62. Pipes de datosPipes de datos @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: [s1, s2, descrip, resStr] << new File("testdata.csv").readLines() .collect {line -> line.tokenize(",")} res = Integer.parseInt(resStr) }
  • 63. Pipes de datosPipes de datos @Unroll def "distance on #descrip"() { expect: LevenshteinCalculator.getLevenshteinDistance(s1, s2) == res (res == 0? s1 == s2 : s1 != s2) where: [s1, s2, descrip, resStr] << new File("testdata.csv").readLines() .collect {line -> line.tokenize(",")} res = Integer.parseInt(resStr) } Asignaciones de variables
  • 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
  • 66. StubsStubs def "check valid comics"() { given: def apiStub = Stub(MarvelApi) { findComicsByCharacter(_) >> [ new MarvelComic(id: 1, date: new Date(), creators: [new ComicCreator(id: 101)] ), (............) new MarvelComic(id: 6, date: null, creators: [new ComicCreator(id: 103)] ) ] } MarvelQuestionnaireFactory f = new MarvelQuestionFactory(apiStub) expect: f.loadValidQuestionnarieComics(1)*.id == [1, 5] }
  • 67. StubsStubs def "check valid comics"() { given: def apiStub = Stub(MarvelApi) { findComicsByCharacter(_) >> [ new MarvelComic(id: 1, date: new Date(), creators: [new ComicCreator(id: 101)] ), (............) new MarvelComic(id: 6, date: null, creators: [new ComicCreator(id: 103)] ) ] } MarvelQuestionnaireFactory f = new MarvelQuestionFactory(apiStub) expect: f.loadValidQuestionnarieComics(1)*.id == [1, 5] } Stub de una clase añadir dependencias a cglib-nodep y objenesis
  • 68. StubsStubs def "check valid comics"() { given: def apiStub = Stub(MarvelApi) { findComicsByCharacter(_) >> [ new MarvelComic(id: 1, date: new Date(), creators: [new ComicCreator(id: 101)] ), (............) new MarvelComic(id: 6, date: null, creators: [new ComicCreator(id: 103)] ) ] } MarvelQuestionnaireFactory f = new MarvelQuestionFactory(apiStub) expect: f.loadValidQuestionnarieComics(1)*.id == [1, 5] } Named parameter constructor
  • 69. Stubs: constraintsStubs: constraints Método: admite expresiones regulares api./findComicsBy.*/(...) Propiedad (getter) api.apiKey Argumentos stub.metodo("hello") stub.metodo(!"hello") stub.metodo() stub.metodo(_) stub.metodo(*_) stub.metodo(_ as String) stub.metodo({ l -> l.size() > 3 })
  • 70. Stubs: comportamientoStubs: comportamiento Siempre devolver mismo valor (>>) stub.metodo(args) >> result1 Devolver valores secuencialmente (>>>) stub.metodo(args) >>> [res1, res2, res3] Ejecución de código (cambio estado, calcular retorno) stub.metodo(...) >> { args -> ..... } stub.metodo(...) >> { arg -> ..... } Encadenar llamadas de distinto tipo stub.metodo(args) >>> [r1, r2] >> { (code) } >> r4 Llamada no declarada: valor por defecto / objeto vacío (no null)
  • 71. Tests basados enTests basados en InteraccionesInteracciones
  • 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!!!
  • 86. ¿Ibas a alguna parte?...
  • 87. ¡¡¡¿¿¿SOMOS HOMBRES¡¡¡¿¿¿SOMOS HOMBRES O NENAZAS???!!!O NENAZAS???!!! ¿Ibas a alguna parte?...
  • 88. Tests de integraciónTests de integración
  • 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
  • 95. SpringSpring @ContextConfiguration(locations = "classpath:spring/application-config.xml") class CourseRestControllerSpec extends Specification { @Autowired @Subject CourseRestController controller def "get courses"() { when: ListPage<Course> courses = controller.getCourses(new PaginationDesc(from: 1, max: 10)) then: courses.listSize == 7 courses.elements[2].title == "Intensivo de rueda cubana" } }
  • 96. SpringSpring @ContextConfiguration(locations = "classpath:spring/application-config.xml") class CourseRestControllerSpec extends Specification { @Autowired @Subject CourseRestController controller def "get courses"() { when: ListPage<Course> courses = controller.getCourses(new PaginationDesc(from: 1, max: 10)) then: courses.listSize == 7 courses.elements[2].title == "Intensivo de rueda cubana" } } spock-spring
  • 98. Tests web funcionalesTests web funcionales class QuestionnariesPageSpec extends GebSpec { def "questionnaries page check"() { final EXPECTED_ELEMENT = "Cuestionario chulo" given: go "/es/questionnaries" expect: $("p.recordcount > .valor").text() == "7" and: def link = $("ol.pag-registros > li .media-heading a")[5] link.text() == EXPECTED_ELEMENT when: link.click() then: title == EXPECTED_ELEMENT } }
  • 99. Tests web funcionalesTests web funcionales class QuestionnariesPageSpec extends GebSpec { def "questionnaries page check"() { final EXPECTED_ELEMENT = "Cuestionario chulo" given: go "/es/questionnaries" expect: $("p.recordcount > .valor").text() == "7" and: def link = $("ol.pag-registros > li .media-heading a")[5] link.text() == EXPECTED_ELEMENT when: link.click() then: title == EXPECTED_ELEMENT } }
  • 100. GebGeb Very Groovy Browser Automation Basado en Selenium http://www.gebish.org/ Permite hacer capturas (reporting) <dependency> <groupId>org.gebish</groupId> <artifactId>geb-spock</artifactId> <version>0.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-htmlunit-driver</artifactId> <version>2.26.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-firefox-driver</artifactId> <version>2.26.0</version> <scope>test</scope> </dependency> Dependencias Configuracioń: /GebConfig.groovy (DSL) import org.openqa.selenium.htmlunit.HtmlUnitDriver; driver = { new HtmlUnitDriver() } baseUrl = "http://xxxxxxxxxxxxxxxxx" Instalar Drivers (PhantomJS, Firefox...)
  • 101. Geb con Page ObjectsGeb con Page Objects class PaginationModule extends Module { def root static content = { paginationbar { root.find(".paginationbar") } total { paginationbar.find (".recordcount .valor").text() as int } pageElements { root.find("ol.pag-registros > li") } } } class QuestionnarieHeader extends Module { def root static content = { link { root.find(".media-heading a") } description { link.text() } } } class QuestionnariesListPage extends Page { static url = "/es/questionnaries" static at = { title == "Registros" } static content = { pagination { module PaginationModule, root: $(".sumario_registros .pagination-container") } questionnaries { pagination.pageElements.collect { module QuestionnarieHeader, root: it } } } }
  • 102. Geb con Page ObjectsGeb con Page Objects class PaginationModule extends Module { def root static content = { paginationbar { root.find(".paginationbar") } total { paginationbar.find (".recordcount .valor").text() as int } pageElements { root.find("ol.pag-registros > li") } } } class QuestionnarieHeader extends Module { def root static content = { link { root.find(".media-heading a") } description { link.text() } } } class QuestionnariesListPage extends Page { static url = "/es/questionnaries" static at = { title == "Registros" } static content = { pagination { module PaginationModule, root: $(".sumario_registros .pagination-container") } questionnaries { pagination.pageElements.collect { module QuestionnarieHeader, root: it } } } } Page object - url: para ir a la página - at: para comprobar si estamos en ella - content: acceso rápido a elementos
  • 103. Geb con Page ObjectsGeb con Page Objects class PaginationModule extends Module { def root static content = { paginationbar { root.find(".paginationbar") } total { paginationbar.find (".recordcount .valor").text() as int } pageElements { root.find("ol.pag-registros > li") } } } class QuestionnarieHeader extends Module { def root static content = { link { root.find(".media-heading a") } description { link.text() } } } class QuestionnariesListPage extends Page { static url = "/es/questionnaries" static at = { title == "Registros" } static content = { pagination { module PaginationModule, root: $(".sumario_registros .pagination-container") } questionnaries { pagination.pageElements.collect { module QuestionnarieHeader, root: it } } } } Module object Elemento reutilizable por varias páginas
  • 104. Geb con Page ObjectsGeb con Page Objects class PaginationModule extends Module { def root static content = { paginationbar { root.find(".paginationbar") } total { paginationbar.find (".recordcount .valor").text() as int } pageElements { root.find("ol.pag-registros > li") } } } class QuestionnarieHeader extends Module { def root static content = { link { root.find(".media-heading a") } description { link.text() } } } class QuestionnariesListPage extends Page { static url = "/es/questionnaries" static at = { title == "Registros" } static content = { pagination { module PaginationModule, root: $(".sumario_registros .pagination-container") } questionnaries { pagination.pageElements.collect { module QuestionnarieHeader, root: it } } } } Forma de usar los módulos dentro de un page object
  • 105. Geb con Page ObjectsGeb con Page Objects class QuestionnariesPageSpec extends GebSpec { def "questionnaries page check"() { final EXPECTED_ELEMENT = "Cuestionario chulo" given: to QuestionnariesListPage expect: at QuestionnariesListPage and: pagination.total == 7 and: def quest = questionnaries[5] quest.description == EXPECTED_ELEMENT when: quest.link.click() then: waitFor { at QuestionnariePage } questionnarieTitle == EXPECTED_ELEMENT } }
  • 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/
  • 113. Gracias por la atención...Gracias por la atención... Andrés ViedmaAndrés Viedma @andres_viedma@andres_viedma