tests en java con groovy y spock
DESCRIPTION
No hay forma, por más que pruebas bibliotecas Java de tests no encuentras una forma de hacerlos sencilla y potente y que realmente te convenza del todo. Tu código de pruebas a menudo acaba siendo un pequeño batiburrillo ilegible que prefieres tocar lo mínimo posible, y que incluso te quita las ganas de hacer más tests. Has oído que la gente de Groovy habla muy bien de Spock, pero no te fías mucho de esos paganos que hacen guarreos con el código y no adoran debidamente al gran dios J. En esta charla, que se presentó el 26/6/2014 organizada por MadridJUG y MadridGUG, Andrés Viedma nos mostrará lo sencillo que es integrar Spock en un proyecto Java, y cómo su gran expresividad y la potencia de Groovy nos pueden ayudar a crear tests en los que puedas preocuparte más de qué quieres probar que de cómo tienes que programar la prueba, y que además sirvan para documentar de una forma muy elegante cuál es el comportamiento esperado de nuestra querida aplicación Java. Se explorará además cómo Spock puede encajar perfectamente no solo con TDD sino también con la automatización de tests funcionales sobre la web, e incluso con técnicas de más alto nivel como BDD y metodologías ágiles en general.TRANSCRIPT
SPOCKSPOCK
Pruebas en Java con Groovy y Pruebas en Java con Groovy y
Andrés ViedmaAndrés Viedma
¿Quién soy?¿Quién soy?
Dinosaurio del softwaremás de 20 años como profesional
Javero inquieto
Sospechoso habitual del MadridGUG y MadridJUG
Escribo en Apaga y vuelve a encenderhttp://apagayvuelveaencender.blogspot.com
Andrés ViedmaAndrés Viedma@andres_viedma@andres_viedma
Pero... ¿de verdad hacemos Pero... ¿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 esimportante.
EL INFORMÁTICO VAGOEL INFORMÁTICO VAGO
Hacer pruebas esimportante.
Es una pena quetambié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 esovoy a necesitar...
¡Uf! Para montar esovoy 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'
// spocktestCompile '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
LegibilidadWhen/then: efectos lateralesExpect: método funcional puro
and: encadenar varios bloques del mismo
tipo
Condiciones then / expectCondiciones then / expect
when:stack.push(elem)
then:!stack.emptystack.size() == 1stack.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
@Ignoredef "esta no se va a ejecutar"() { }
@Ignore(reason = "porque no funciona ni p'atrás")def "esta tampoco se va a ejecutar"() { }
@IgnoreRestdef "si lo pongo esta va a ser la única en ejecutarse"() { }
@IgnoreIf({ os.windows })def "esta solo se ejecutaría en Windows"() { }
@Stepwiseclass 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ónselectiva
Timeout
Orden deejecución
Tests basados enTests basados enDatosDatos
@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 claseañ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 enInteraccionesInteracciones
ExternalEvent Log
System
QuestionnaireDAO DB
Event LogAPI
QuestionnaireService
No hay resultado que probar
Añadir unAñadir uncuestionariocuestionario
Tests de Interacciones: por quéTests de Interacciones: por qué
ExternalEvent Log
System
QuestionnaireDAO DB
Event LogAPI
QuestionnaireService
No hay resultado que probar
Añadir unAñadir uncuestionariocuestionario
Tests de Interacciones: por quéTests de Interacciones: por qué
¡¡¡N
O L
O 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 HOMBRESO 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
TestsTestsFuncionalesFuncionales
(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 objectElemento 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 deAceptació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 }}
BDDBehaviour 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 Consolehttp://meet.spockframework.org/
Proyecto de ejemplohttp://files.spockframework.org/spock-example-0.5-groovy-1.7.zip
Lenguaje Groovyhttp://beta.groovy-lang.org/docs/groovy-2.3.1/html/documentation/#_lists
Modificaciones Groovy a librería estándar JDKhttp://groovy.codehaus.org/groovy-jdk/
Más informaciónMás información
Gebhttp://www.gebish.org/
spock-spring http://code.google.com/p/spock/wiki/SpringExtension
spock-dbunithttps://github.com/janbols/spock-dbunit
Gracias por la atención... Gracias por la atención...
Andrés ViedmaAndrés Viedma@andres_viedma@andres_viedma