Spock Framework

O objetivo desta aula é a familiarização com a spock framework, objeto de estudo na primeira entrega do projeto. De seguida, apresenta-se as características principais da framework. Para informações mais detalhadas, consultar a documentação oficial. A inspiração para este tutorial está aqui.

Spock é uma framework para testar software baseado na linguagem Groovy. Esta framework pretende ser uma alternativa mais poderosa para a stack JUnit, recorrendo para isso à expressividade e funcionalidades do Groovy. Sendo Groovy uma linguagem baseada na JVM, esta integra-se perfeitamente com o Java. Além dessa interoperabilidade, Groovy oferece conceitos adicionais, como por exemplo, tipos opcionais e meta-programação.

1. Maven Dependency

Para utilizar a spock framework num projeto maven, é necessário adicionar as seguintes dependências, ${version-spock} deve ser previamente declarada com a versão que se pretende usar (ver em https://mvnrepository.com/artifact/org.spockframework/spock-core):

<dependency>    
   <groupId>org.spockframework</groupId>    
   <artifactId>spock-core</artifactId>    
   <version>${version.spock}</version>    
   <scope>test</scope>
</dependency>
<dependency>    
   <groupId>org.codehaus.groovy</groupId>    
   <artifactId>groovy-all</artifactId>    
   <version>${version.groovy}</version>    
   <scope>test</scope>
</dependency>

Adicionamos Spock e Groovy como faríamos com qualquer outra biblioteca. No entanto, como Groovy é outra linguagem JVM, precisamos incluir o plugin gmavenplus, para conseguirmos compilar e executar o seu código:

<plugin>
    <groupId>org.codehaus.gmavenplus</groupId>
    <artifactId>gmavenplus-plugin</artifactId>
    <version>${version.gmavenplus}</version>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>compileTests</goal>
            </goals>
        </execution>
     </executions>
</plugin>

Com as dependências no pom, podemos escrever o nosso primeiro teste em spock. Note que estamos a usar o Groovy e Spock apenas para fins de testes e é por isso que essas dependências têm o scope test.

2. Estrutura de um teste spock

2.1 Especificações e funcionalidade
Como estamos a escrever os testes em Groovy, então devemos adicionar os testes ao diretório src/test/groovy, em vez de src/test/java. Como exemplo, vamos criar o nosso primeiro teste neste diretório: 
class FirstSpecification extends Specification { 
}
Observe que estamos a estender a interface de especificação (Specification). Cada classe Spock deve estender esta classe para que possa usufruir da spock framework. Assim, podemos começar a criar a nossa primeira funcionalidade (feature):
def "one plus one should equal two"() {  
   expect:  1 + 1 == 2
}

Antes de explicar o código, vale a pena referir que funcionalidade (feature) em Spock é equivalente a um teste em JUnit. 

Agora, vamos analisar a feature que acabamos de criar. Existem algumas diferenças entre esta e o que normalmente faríamos em Java:

  • A primeira diferença é que o nome do método da feature é escrito como uma cadeia de caracteres. Em JUnit, teríamos tido um nome de método que usa CamelCase ou sublinhados para separar as palavras, o que não teria sido tão expressivo ou legível.
  • A próxima é que o nosso código de teste reside num bloco expect. Iremos falar de blocos de seguida, mas, essencialmente, blocos são uma forma lógica de dividir as diferentes etapas dos testes.
  • Finalmente, percebemos que não há asserções. Isso porque a afirmação está implícita: o teste passa quando a instrução é igual a true e falha quando é false
2.2 Blocos (blocks)
Em JUnit não há uma forma expressiva de separar um teste em várias partes. Por exemplo, se estivéssemos a seguir o desenvolvimento orientado por comportamentos (behavioural driven development), teríamos de dividir o teste seguinte utilizando comentários:

@Test
public void givenTwoAndTwo_whenAdding_thenResultIsFour() {   
    // Given   
    int first = 2;   
    int second = 2;    

    // When   
    int result = first + second;    

    // Then   
    assertTrue(result == 4);
}

Spock aborda esse problema utilizando blocos. Os blocos são uma forma de separar as várias fases do teste utilizando labels. As labels mais comuns são as seguintes:

  1. given (também setup): é neste bloco onde colocamos a configuração necessária antes de um teste ser executado. 
  2. when: é neste bloco onde é fornecido o estímulo para o que irá ser testado. 
  3. then: é neste bloco onde colocamos as asserções
  4. expect: este bloco é utilizado para ter um estímulo e asserção dentro do mesmo bloco, usado quando a resposta ao estímulo é avaliada como uma expressão.
  5. cleanup: neste bloco, o sistema é colocado na versão antes de o teste ter sido executado (pode ser utilizado para, por exemplo, apagar ficheiros que o teste tenha criado)
Utilizando a notação de blocos, o teste anterior seria expresso da seguinte forma (o que o torna mais compreensível):
def "two plus two should equal four"() 
{    
   given:        
   def first = 2     //exemplo com def   
   int second = 2    //exemplo com int 

   when:        
   def result = first + second     

   then:        
   result == 4
}
2.2 Asserções (Asserts) através das funcionalidades do Groovy

Nos blocos then e expect, asserções (asserts) não precisam de ser utilizados de forma explícita. Nesses blocos, cada instrução é avaliada, falhando se não for verdadeira. Vamos tentar uma asserção de lista para demonstrar isso:

def "Should be able to remove from list"() 
{    
   given:        
   def list = [1, 2, 3, 4]     

   when:        
   list.remove(0)     

   then:        
   list == [2, 3, 4]
}

Apesar desta explicação não ser dedicada ao Groovy, é interessante explicar o que está a acontecer neste teste.

Primeiro, o Groovy fornece formas mais simples de criar listas. Em segundo lugar, tirando partido do fato do Groovy ser uma linguagem dinâmica, é possível usar def. Finalmente, simplificando dessa forma o teste, a característica mais útil demonstrada é a sobrecarga do operador de comparação: em vez de fazer uma comparação de referência, como em Java, o método Equals() será invocado para comparar as duas listas.

Spock também fornece uma forma expressiva de verificar exceções:

def "Should get an index out of bounds when removing a non-existent item"() {
    given:
        def list = [1, 2, 3, 4]
  
    when:
        list.remove(20)
 
    then:
        thrown(IndexOutOfBoundsException)
        list.size() == 4
}
2.3 Datatables (data driven development)

Quanto comparado com o JUnit, a framework Spock oferece mecanismos bem mais simples para implementar testes parametrizados (data-driven testing). Isso é conseguido da seguinte forma:

@Unroll('testing power of 2: #a, #b ==> #c')   //this annotation is optional
def "numbers to the power of two"(int a, int b, int c) {
  expect:
      Math.pow(a, b) == c
 
  where:
      a | b | c
      1 | 2 | 1
      2 | 2 | 4
      3 | 2 | 9
  }

Uma das vantagens do Spock é reduzir a quantidade de boilerplate code quando comparado com o JUnit.

3. Exercício

Nesta aula vamos utilizar a Spock framework num subsistema do sistema, bastante simplificado, Adventures. Para isso necessitamos de trabalhar numa versão do código preparada para esta aula, de modo a não interferir com o código que se encontra a ser desenvolvido para a entrega da primeira parte do projeto. Assim, cada aluno irá trabalhar no seu repositório pessoal no git:

  • Aceder, usando a interface do GitHub, ao repositório https://github.com/tecnico-softeng/aula-spock, e efetuar um fork (opção no canto superior direito)
  • Fazer git clone do seu repositório pessoal
  • Executar mvn install
  • Abrir o projeto no IDE como um projeto maven

Converta os testes do módulo bank para testes spock.


=== FAQ ===

Q1: os *testes de Spock*, em groovy, aparentemente *não correm*. Fiz testes que falham de propósito e estes não correm!
A1: Depois de fazer o que diz no guião do lab 2,
1)verificar que o maven-surefire-plugin está pom geral, em build > pluginManagement > plugins:
                 <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.17</version>
                </plugin>

2) Verificar que os nomes dos ficheiros de teste .groovy têm o nome da classe que contêm*. Ou seja, para

import spock.lang.Specification
class MyTest extends Specification {
    def "one plus one should equal two"() {
    }
...
}

usar MyTest.groovy como nome para o ficheiro.

3) para correr os testes, usar mvn test. Na dúvida, mvn clean test