Crie testes unitários mais amigáveis e mais rapidamente com Spock

leandrostl

Leandro Lima

Posted on August 10, 2020

Crie testes unitários mais amigáveis e mais rapidamente com Spock

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, a ferramenta, é um framework que traz o poder do Groovy 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 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.

Dependências

Criaremos um projeto sem arquétipo através do Maven e adicionaremos o plugin do GMaven, necessário para integrar o Groovy ao Maven, e o Byte Buddy, sem dependências, necessário para criação de Mocks.



<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>


Enter fullscreen mode Exit fullscreen mode

Segue um comparativo 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:



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
    }
}


Enter fullscreen mode Exit fullscreen mode

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

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



class Especificacao extends Specification {
  // fields
  // fixture methods
  // feature methods
  // helper methods
}


Enter fullscreen mode Exit fullscreen mode

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:



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
    }
}


Enter fullscreen mode Exit fullscreen mode

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:



// 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() {}


Enter fullscreen mode Exit fullscreen mode

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)

Fluxo de fases da especificação. Fonte: Spock
  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().



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)
    }
}


Enter fullscreen mode Exit fullscreen mode

É 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.



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")
    }
}


Enter fullscreen mode Exit fullscreen mode

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 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.



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."),
        ]
    }
}


Enter fullscreen mode Exit fullscreen mode

Esse é o resultado esperado do teste acima utilizando a biblioteca Spock Report Extension.

Avaliando as grandes mulheres das Ciências da Computação

Aqui está nosso intruso!

Dennis Ritchie é um grande personagem da computação, mas não luta como uma mulher!

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.



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
    }
}




Enter fullscreen mode Exit fullscreen mode

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…

💖 💪 🙅 🚩
leandrostl
Leandro Lima

Posted on August 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related