Crie testes unitários mais amigáveis e mais rapidamente com Spock
Tags: bdd, tutorial, testing, java
Published: 2020-08-10 18:57:02

Testes e especificações quase sempre são uma dor de cabeça para o desenvolvedor. Gastamos muito tempo escrevendo testes, às vezes muito mais tempo do que gastamos com o desenvolvimento. É difícil também nos ater exclusivamente à linguagem do negócio e escrever algo significativo. Bem mais fácil é nos apoiar nos resultados esperados do código. Assim qualquer alternativa para focar no negócio e fazer testes mais rapidamente é válida.

[Spock](http://spockframework.org/), a ferramenta, é um framework que traz o poder do [Groovy](https://groovy-lang.org/) para tornar testes em geral mais expressivos, mais rápidos de escrever, mais agradáveis de ler e entender seguindo de maneira mais descritiva os passos de uma [especificação](http://xunitpatterns.com/Four%20Phase%20Test.html) de quatro fases.

O objetivo desse artigo é fazer uma varredura superficial das funcionalidades desse framework.

O código usado nesse material está disponível [aqui](https://github.com/leandrostl/spock-test).

## Dependências

Criaremos um projeto sem arquétipo através do [Maven](http://maven.apache.org/) e adicionaremos o plugin do [GMaven](https://groovy.github.io/GMavenPlus/), necessário para integrar o Groovy ao Maven, e o [Byte Buddy](https://bytebuddy.net/), sem dependências, necessário para criação de Mocks.
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.leandro</groupId>
    <artifactId>spock-maven-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <description>Projeto para estudar como aplicar testes com Spock.</description>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>2.0-M2-groovy-3.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>
        <!-- Byte Buddy permite manipular bytecodes em tempo de execução.
        Isso permite ao Spock criar Mocks sem necessidade de qualquer outra
        biblioteca dedicada. Outra alternativa ao Byte Buddy é a CGLIB. -->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.10.13</version>
            <scope>test</scope>
        </dependency>
        <!-- Opcional para usar banco de dados H2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
            <scope>test</scope>
        </dependency>

        <!-- Permite criar relatórios para os testes. -->
        <dependency>
            <groupId>com.athaydes</groupId>
            <artifactId>spock-reports</artifactId>
            <version>2.0-RC2</version>
            <scope>test</scope>
            <!-- Garante não afetar a versão do Groovy/Spock -->
            <exclusions>
                <exclusion>
                    <groupId>*</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Necessário para criar relatórios -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- O GMaven Plus permite código Groovy ser
            compilado e executado em projetos Maven. -->
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>1.9.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compileTests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <configuration>
                    <useFile>false</useFile>
                    <includes>
                        <include>**/*Test.java</include>
                        <include>**/*Spec.java</include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```
Segue um [comparativo](https://www.jrebel.com/blog/java-code-generation-libraries-comparison) entre o Byte Buddy e o CGLIB.

Os testes usando o Spock serão colocados na pasta `src/test/groovy*.*`

Vamos começar sentindo o gosto do framework criando um teste simples, mas que apresenta um pouco do poder de expressão:

```groovy
import spock.lang.Specification
import spock.lang.Unroll

class MaxTest extends Specification {
    @Unroll
    def "Verificar se o máximo entre #a e #b é #c"() {
        expect:
        Math.max(a, b) == c

        where:
        a  | b | c
        1  | 2 | 2
        5  | 2 | 5
        0  | 1 | 1
        -1 | 1 | 1
    }
}
```

Dados três números, a, b e c, desejamos verificar se nossa aplicação, que calcula o máximo entre a e b, retorna c para cada um dos casos na tabela.

Os resultados esperados são esses:

![Resultados esperados](https://dev-to-uploads.s3.amazonaws.com/i/j8hnjdx84xxui085rsb4.png)


Com o Spock, escrevemos especificações, e para isso, estendemos a classe *Specification*. Uma especificação é composta de

```Groovy 
class Especificacao extends Specification {
  // fields
  // fixture methods
  // feature methods
  // helper methods
}
```
## Fields

São equivalentes aos campos de classes Java, variáveis com escopo de classe. Entretanto, há uma diferença, a inicialização junto da declaração é equivalente a inicia-los dentro de um método anotado com `@BeforeEach`. O framework faz dessa forma para isolar os métodos. É uma boa prática inicializa os campos no mesmo local da declaração.

> Campos são variáveis de escopo de classe com instâncias individuais por escopo de método.

Quando temos objetos muito grandes, que precisam de muito processamento para serem instanciados, é conveniente manter uma instância única e compartilha-la entre os métodos de teste. Para isso, o Spock utiliza a anotação `@Shared`.

Dessa forma, esperamos que os seguintes testes sejam executados com sucesso:

```Groovy
import spock.lang.Shared
import spock.lang.Specification;

class CamposTest extends Specification {
	def normal = 5
	@Shared shared = 3
	
	def "Se alterar o valor do campo, deve ser verificável"() {
		given: 'Valor dos campos foram alterados'
		normal = 6
		shared = 4
		
		expect: 'O novo valor seja observado no resultado'
		normal == 6
		shared == 4
	}
	
	def "Sem qualquer alteração, mantem o valor do campo alterado anteriormente"() {
		given: 'Não há novas alterações nos campos'
		expect: 'Campo normal deve ter o valor antigo'
		normal == 5
		and: 'Campo shared deve ter o valor definido no método anterior'
		shared == 4
	}
}
```

Outra possibilidade é usar `static final fields`. A documentação sugere dar preferência pelo`@Shared` e deixar esse caso apenas para constantes por uma questão semântica.

## Fixture Methods

São responsáveis por configurar o ambiente de testes, inicializando e finalizando os testes. São eles:

```Groovy
// Equivale ao @BeforeAll/@BeforeClass do JUnit. Roda apenas uma vez // antes que os testes se iniciem.
def setupSpec() {}    
// Equivale ao @Before do JUnit. Roda antes de cada método.
def setup() {}    
// Equivale ao @After do JUnit. Roda logo após cada método.    
def cleanup() {}     
// Equivale ao @AfterAll/AfterClass do JUnit. Roda uma única vez ao //término do último teste.
def cleanupSpec() {}
```
## Feature Methods

O coração de tudo! Descrevem a especificação, o comportamento esperado da sua aplicação. São métodos nomeados por frases auto-explicativas e compostos por blocos indicando as fases da especificação:

![Fluxo de fases da especificação. Fonte: [Spock](http://spockframework.org/spock/docs/1.3/spock_primer.html)](http://spockframework.org/spock/docs/1.3/images/Blocks2Phases.png)
<figcaption>Fluxo de fases da especificação. Fonte: <a href="http://spockframework.org/spock/docs/1.3/spock_primer.html">Spock</a></figcaption>

 1. **GIVEN**: onde você faz a inicialização do teste, isto é, inicializa variáveis. Pode estar implícito, ou seja, não precisa ser declarado.

 2. **WHEN**: representa um estímulo ao teste, uma entrada, uma ação externa. Sempre que acontece um estímulo, esperamos então (*then*) uma resposta.

 3. **THEN**: resposta a um estímulo, o que esperar após um bloco *when*. De fato, esses blocos ocorrem juntos e pode haver mais de um par when/then no método.
Da mesma forma que *expect*, esse bloco sempre trata de resultado de condições. Vamos tratar de condições mais a frente.

 4. **EXPECT**: parecido com o *then*, é mais natural para casos onde não é interessante usar um bloco *when*. Isso ocorre especialmente em métodos puramente funcionais, isto é, sem efeitos colaterais. 
Juntando o estímulo do teste à resposta em uma única expressão, temos uma apresentação mais concisa.

 5. **CLEANUP**: o local adequado para finalizar o uso de recursos como acesso a arquivos, streams, etc.

 6. **WHERE**: é utilizado para escrever testes parametrizados. Cada conjunto de dados dentro do bloco *where* é tratado como um novo teste.

## Condições

### Explícitas ou implícitas

Condições ou *assertions*, testam a sua aplicação. Elas podem ser implícitas, dentro dos blocos *when/then* ou dentro do bloco *expect*. Tudo declarado dentro desses blocos compõem uma ou mais condições. Elas também podem ser explícitas, fora desses blocos. Para tanto, usa-se a palavra-chave `assert`, ex.: `assert stack.empty`.

### Exceções

Tratam casos em que se espera uma exceção do código testado. Para testar exceções, usam-se os métodos `thrown()` e `notThrown()`.

 ```Groovy
import spock.lang.Specification

import static java.util.Collections.emptyList

class ExcecaoTest extends Specification {

    def "Lançar exceção quando tentar tirar um elemento de uma lista vazia"() {
        given: 'Uma lista vazia'
        def lista = emptyList()

        when: 'Tentar remover um elemento desta lista'
        lista.pop()

        then: 'Deve lançar exceção do tipo NoSuchElementException'
        def e = thrown(NoSuchElementException)
        and: 'Causa deve ser nula'
        e.cause == null
    }

    def "Não lançar exceção quando tentar incluir uma chave nula num mapa"() {
        given: 'Um hashmap'
        def mapa = new HashMap()

        when: 'Tentar inserir um par chave/valor com chave nula'
        mapa.put(null, 1)

        then: 'Não deve ocorrer qualquer exceção'
        noExceptionThrown()
    }

    def "Groovy == equivale a equals em Java e é nullsafe"() {
        when: 'Comparar null com qualquer objeto'
        def isLeiaNula = "General Leia Organa" == null
        def isHanNulo = null == "Han Solo"
        then: 'Deve retornar false e não gerar exceção de null pointer'
        !isLeiaNula && !isHanNulo
        notThrown(NullPointerException)
    }
}
```

É possível acessar o conteúdo da exceção atribuindo o resultado do método a uma variável, `def e = thrown(EmptyStackException)` ou ainda, `EmptyStackException e = thrown()`.

### Help Methods

Usando outros frameworks de teste unitário é comum adotarmos métodos utilitários para não duplicar código. No *Groovy* não seria diferente. O exemplo a seguir mostra o poder da linguagem.

```Groovy
import spock.lang.Specification

class HelpMethodsTest extends Specification {

    def "Verificar se email obedece padrão"() {
        expect: 'Meu email fake seja válido!'
        "é um email válido"("leandro@leandro.com.br")
    }

    static def "é um email válido"(email) {
        email.contains('@') && email.endsWith(".com.br")
    }
}
```

Para identificar melhor onde está o problema, sugere-se usar o `assert` dentro do método utilitário para cada linha condicional.

### Usando o método `with` para verificar campos

O [Groovy](http://docs.groovy-lang.org/docs/next/html/documentation/style-guide.html#_using_code_with_code_and_code_tap_code_for_repeated_operations_on_the_same_bean) oferece uma gama muito grande de syntactic sugars que nos permitem deixar o código muito mais simples. Um deles é o método `with` que permite testar as propriedades de um objeto em poucas linhas até que qualquer dessas propriedades seja falsa. Uma alternativa ao `with` é o `verifyAll` que testa todos os campos sem o curto circuito.

```Groovy
import groovy.transform.ToString
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll

@Unroll
class WithTest extends Specification {

    @Shared
            mulheresImportantes = [
                    "Ada Lovelace",
                    "Grace Hopper",
                    "Margaret Hamilton",
                    "Katie Bouman",
                    "Barbara Liskov"
            ]

    @ToString
    static class PessoasImportanteDaTI {
        String nome
        String sexo
        String descricao
    }


    def "Verificar se #pessoa.nome é uma mulher importante das ciências da computação"() {
        verifyAll(pessoa) {
            nome in mulheresImportantes
            sexo == 'F'
        }

        where:
        pessoa << [
                new PessoasImportanteDaTI(
                        nome: "Ada Lovelace",
                        sexo: 'F',
                        descricao: "Escreveu o primeiro algoritmo\
					para ser processado por uma máquina. "),
                new PessoasImportanteDaTI(
                        nome: "Grace Hopper",
                        sexo: 'F',
                        descricao: "Criou uma linguagem que foi a \
					base para o COBOL."),
                new PessoasImportanteDaTI(
                        nome: "Katie Bouman",
                        sexo: 'F',
                        descricao: "Criou algotimos para processar \
					a primeira imagem de um buraco negro."),
                new PessoasImportanteDaTI(
                        nome: "Barbara Liskov",
                        sexo: 'F',
                        descricao: "Criou o Princípio da Substituição de Liskov, \
					foi a primeira mulher a obter um PhD em Ciência da Computação \
					nos Estados Unidos e inventou o Tipo Abstrato de Dado."),
                new PessoasImportanteDaTI(
                        nome: "Margaret Hamilton",
                        sexo: 'F',
                        descricao: "Desenvolveu o programa de voo usado no projeto Apollo 11."),
                new PessoasImportanteDaTI(
                        nome: "Dennis Ritchie",
                        sexo: 'M',
                        descricao: "Criou a linguagem de programação C."),
        ]
    }
}
```

Esse é o resultado esperado do teste acima utilizando a biblioteca [Spock Report Extension](https://github.com/renatoathaydes/spock-reports).

![Avaliando as grandes mulheres das Ciências da Computação](https://dev-to-uploads.s3.amazonaws.com/i/olm28xc5fertmgvsx1n2.png)

Aqui está nosso intruso! 

![Dennis Ritchie é um grande personagem da computação, mas não luta como uma mulher!](https://dev-to-uploads.s3.amazonaws.com/i/nhw2dfzzx44tqkcoca5k.png)

## Mocking

Um Teste Unitário só pode ser chamado assim se for isolado do mundo externo, isto quer dizer, testa apenas aquela unidade de código mínima e mock as dependências. O Spock oferece meios bastante elegantes para isso.

```Groovy
import groovy.sql.Sql
import groovy.transform.EqualsAndHashCode
import spock.lang.Specification

class MockTest extends Specification {

    @EqualsAndHashCode
    class Carta {
        Integer id
        String nome
    }

    class MagicRepository {
        Sql sql

        MagicRepository() {
            sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
            sql.execute '''
              CREATE TABLE carta (
                id INTEGER PRIMARY KEY AUTO_INCREMENT, 
                nome VARCHAR(64)
              )'''
        }

        Carta addCarta(nome) {
            sql.execute "INSERT INTO carta(nome) VALUES ${nome}"
            findByNome(nome)
        }

        Carta findById(Integer id) {
            mapCarta sql.firstRow("select * from carta where id = ${id}")
        }

        Carta findByNome(String nome) {
            mapCarta sql.firstRow("select * from carta where nome = ${nome}")
        }

        private Carta mapCarta(retorno) {
            new Carta().tap {
                id = retorno.ID
                nome = retorno.NOME
            }
        }
    }

    class MagicService {
        MagicRepository repository

        Carta findById(Integer id) { repository.findById(id) }
    }

    def 'Stub permite apenas definir comportamento de métodos'() {
        given: 'Repository é apenas um stub'
        def carta = new Carta(nome: 'Mana Crypt')
        def stubRepository = Stub(MagicRepository) {
            findById(_ as Integer) >> { Integer id -> carta.tap { id: id } }
        }
        def service = new MagicService(repository: stubRepository)

        expect: 'O carta encontrada deve ser a carta do stub'
        service.findById(1) == carta.tap { id: 1 }
    }

    def "Spy permite saber quantas interações ocorreram e o retorno do método espiado"() {
        given:
        def esperada1 = new Carta(id: 1, nome: 'Black Lotus')
        def esperada2 = new Carta(id: 2, nome: 'Mana Crypt')

        def spyRepository = Spy(MagicRepository) {
            findById(2) >> esperada2
        }
        spyRepository.addCarta("Black Lotus")
        spyRepository.addCarta("Mox Pearl")

        def service = new MagicService(repository: spyRepository)

        when:
        def carta1 = service.findById(1)
        then:
        carta1 == esperada1
        spyRepository.findById(1) == esperada1
        1 * spyRepository.findById(1)

        when:
        def carta2 = service.findById(2)
        then:
        carta2 == esperada2
        spyRepository.findById(2) == esperada2
    }

    def "Mock pode tanto ver a quantidade de interações \
        quanto redefinir comportamento do método"() {
        given: 'Repositório será chamado três vezes'

        def esperada1 = new Carta(id: 1, nome: 'Mox Pearl')
        def esperada2 = new Carta(id: 2, nome: 'Mox Emerald')
        def mockRepository = Mock(MagicRepository) {
            2 * findById(1) >>> [esperada1, esperada2]
            1 * findById(2) >> esperada2
        }

        def service = new MagicService(repository: mockRepository)
        when:
        def carta1 = service.findById(1)
        def carta2 = service.findById(1)
        def carta3 = service.findById(2)
        then:
        carta1 == esperada1
        carta2 == esperada2
        carta3 == esperada2
    }
}


```
O Spock permite usar três categorias de mock:

 1. **Mock**: Substitui o comportamento da classe quando ela é chamada e permite verificar como ela é usada. É o mais conhecido e tem foco em definir um “contrato” de como ele deve ser chamado, que deve responder e quantas vezes.

 2. **Spy**: Não substitui o comportamento, mas permite verificar quando o método real da classe é chamado, o argumento e os resultados;

 3. **Stub**: Apenas substitui comportamento, mas não permite verificar chamadas ao stub e nem testar esse acesso. É uma caixa preta que não pretende ser chamada, mas se for, retorna uma resposta padrão.

### Para continuar estudando…

* [Código desse material](https://github.com/leandrostl/spock-test): vai que a gente melhora algo.

* [Mocks aren’t stubs](https://martinfowler.com/articles/mocksArentStubs.html): Excelente texto do tio Bob

* [Spock and testing RESTful Api](https://dzone.com/articles/spock-and-testing-restful-api): Como ser um framework de testes sem teste de API RESTful?

* [Spock Report Extension](https://github.com/renatoathaydes/spock-reports): Uma extensão bastante útil para mostrar para o chefe.