SlideShare ist ein Scribd-Unternehmen logo
1 von 113
Downloaden Sie, um offline zu lesen
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

Weitere ähnliche Inhalte

Was ist angesagt?

Indexing & Query Optimization
Indexing & Query OptimizationIndexing & Query Optimization
Indexing & Query OptimizationMongoDB
 
Monad as functor with pair of natural transformations
Monad as functor with pair of natural transformationsMonad as functor with pair of natural transformations
Monad as functor with pair of natural transformationsPhilip Schwarz
 
기본적인 테스트에 대한 pytest 자동화 접근
기본적인 테스트에 대한 pytest 자동화 접근기본적인 테스트에 대한 pytest 자동화 접근
기본적인 테스트에 대한 pytest 자동화 접근SangIn Choung
 
Conquering Data Migration from Oracle to Postgres
Conquering Data Migration from Oracle to PostgresConquering Data Migration from Oracle to Postgres
Conquering Data Migration from Oracle to PostgresEDB
 
Docker 사내교육 자료
Docker 사내교육 자료Docker 사내교육 자료
Docker 사내교육 자료Juneyoung Oh
 
Pipeline oriented programming
Pipeline oriented programmingPipeline oriented programming
Pipeline oriented programmingScott Wlaschin
 
SpringBoot and Spring Cloud Service for MSA
SpringBoot and Spring Cloud Service for MSASpringBoot and Spring Cloud Service for MSA
SpringBoot and Spring Cloud Service for MSAOracle Korea
 
Owl: The New Odoo UI Framework
Owl: The New Odoo UI FrameworkOwl: The New Odoo UI Framework
Owl: The New Odoo UI FrameworkOdoo
 
[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱
[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱
[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱PgDay.Seoul
 
10 Rules for Safer Code [Odoo Experience 2016]
10 Rules for Safer Code [Odoo Experience 2016]10 Rules for Safer Code [Odoo Experience 2016]
10 Rules for Safer Code [Odoo Experience 2016]Olivier Dony
 
Testing Spring Boot application in post-JUnit 4 world
Testing Spring Boot application in post-JUnit 4 worldTesting Spring Boot application in post-JUnit 4 world
Testing Spring Boot application in post-JUnit 4 worldYura Nosenko
 
Intro To MongoDB
Intro To MongoDBIntro To MongoDB
Intro To MongoDBAlex Sharp
 
Using Cerberus and PySpark to validate semi-structured datasets
Using Cerberus and PySpark to validate semi-structured datasetsUsing Cerberus and PySpark to validate semi-structured datasets
Using Cerberus and PySpark to validate semi-structured datasetsBartosz Konieczny
 

Was ist angesagt? (20)

Indexing & Query Optimization
Indexing & Query OptimizationIndexing & Query Optimization
Indexing & Query Optimization
 
Monad as functor with pair of natural transformations
Monad as functor with pair of natural transformationsMonad as functor with pair of natural transformations
Monad as functor with pair of natural transformations
 
기본적인 테스트에 대한 pytest 자동화 접근
기본적인 테스트에 대한 pytest 자동화 접근기본적인 테스트에 대한 pytest 자동화 접근
기본적인 테스트에 대한 pytest 자동화 접근
 
Conquering Data Migration from Oracle to Postgres
Conquering Data Migration from Oracle to PostgresConquering Data Migration from Oracle to Postgres
Conquering Data Migration from Oracle to Postgres
 
Docker 사내교육 자료
Docker 사내교육 자료Docker 사내교육 자료
Docker 사내교육 자료
 
Vue JS Intro
Vue JS IntroVue JS Intro
Vue JS Intro
 
Spring mvc
Spring mvcSpring mvc
Spring mvc
 
Pipeline oriented programming
Pipeline oriented programmingPipeline oriented programming
Pipeline oriented programming
 
SpringBoot and Spring Cloud Service for MSA
SpringBoot and Spring Cloud Service for MSASpringBoot and Spring Cloud Service for MSA
SpringBoot and Spring Cloud Service for MSA
 
Love at first Vue
Love at first VueLove at first Vue
Love at first Vue
 
Server side rendering review
Server side rendering reviewServer side rendering review
Server side rendering review
 
Owl: The New Odoo UI Framework
Owl: The New Odoo UI FrameworkOwl: The New Odoo UI Framework
Owl: The New Odoo UI Framework
 
Webpack DevTalk
Webpack DevTalkWebpack DevTalk
Webpack DevTalk
 
[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱
[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱
[Pgday.Seoul 2017] 2. PostgreSQL을 위한 리눅스 커널 최적화 - 김상욱
 
10 Rules for Safer Code [Odoo Experience 2016]
10 Rules for Safer Code [Odoo Experience 2016]10 Rules for Safer Code [Odoo Experience 2016]
10 Rules for Safer Code [Odoo Experience 2016]
 
Testing Spring Boot application in post-JUnit 4 world
Testing Spring Boot application in post-JUnit 4 worldTesting Spring Boot application in post-JUnit 4 world
Testing Spring Boot application in post-JUnit 4 world
 
Intro To MongoDB
Intro To MongoDBIntro To MongoDB
Intro To MongoDB
 
NPM
NPMNPM
NPM
 
Using Cerberus and PySpark to validate semi-structured datasets
Using Cerberus and PySpark to validate semi-structured datasetsUsing Cerberus and PySpark to validate semi-structured datasets
Using Cerberus and PySpark to validate semi-structured datasets
 
Tutorial - REST con java (JAX-RS 2.0)
Tutorial - REST con java (JAX-RS 2.0)Tutorial - REST con java (JAX-RS 2.0)
Tutorial - REST con java (JAX-RS 2.0)
 

Ähnlich wie 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
 

Ähnlich wie 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
 

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