Tests en Java con Groovy y Spock

  • View
    1.840

  • Download
    3

  • Category

    Software

Preview:

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.

Citation preview

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