Introdução ao teste com Spock e Groovy

Introdução ao teste com Spock e Groovy

1. Introdução

Neste artigo, daremos uma olhada emSpock, uma estrutura de testeGroovy. Principalmente, o Spock pretende ser uma alternativa mais poderosa à pilha JUnit tradicional, aproveitando os recursos do Groovy.

Groovy é uma linguagem baseada em JVM que se integra perfeitamente ao Java. Além da interoperabilidade, oferece conceitos adicionais de linguagem, como ser dinâmico, ter tipos opcionais e metaprogramação.

Ao fazer uso do Groovy, Spock apresenta maneiras novas e expressivas de testar nossos aplicativos Java, que simplesmente não são possíveis no código Java comum. Exploraremos alguns dos conceitos de alto nível de Spock durante este artigo, com alguns exemplos práticos passo a passo.

2. Dependência do Maven

Antes de começar, vamos adicionar nossoMaven dependencies:


    org.spockframework
    spock-core
    1.0-groovy-2.4
    test


    org.codehaus.groovy
    groovy-all
    2.4.7
    test

Adicionamos Spock e Groovy como faríamos com qualquer biblioteca padrão. No entanto, como Groovy é uma nova linguagem JVM, precisamos incluir o plug-ingmavenplus para poder compilá-lo e executá-lo:


    org.codehaus.gmavenplus
    gmavenplus-plugin
    1.5
    
        
            
                compile
                testCompile
            
        
     

Agora estamos prontos para escrever nosso primeiro teste de Spock, que será escrito no código Groovy. Observe que estamos usando Groovy e Spock apenas para fins de teste e é por isso que essas dependências têm escopo de teste.

3. Estrutura de um teste de Spock

3.1. Especificações e recursos

Como estamos escrevendo nossos testes em Groovy, precisamos adicioná-los ao diretóriosrc/test/groovy, em vez desrc/test/java.. Vamos criar nosso primeiro teste neste diretório, nomeando-oSpecification.groovy:

class FirstSpecification extends Specification {

}

Observe que estamos estendendo a interfaceSpecification. Cada classe Spock deve estender isso para disponibilizar a estrutura. Isso nos permite implementar nosso primeirofeature:

def "one plus one should equal two"() {
  expect:
  1 + 1 == 2
}

Antes de explicar o código, também é importante notar que, em Spock, o que chamamos defeature é um tanto sinônimo do que vemos comotest em JUnit. Entãowhenever we refer to a feature we are actually referring to a test.

Agora, vamos analisar nossofeature. Ao fazer isso, devemos poder ver imediatamente algumas diferenças entre ele e Java.

A primeira diferença é que o nome do método do recurso é escrito como uma sequência comum. No JUnit, teríamos um nome de método que usa camelcase ou sublinhados para separar as palavras, o que não seria tão expressivo ou legível por humanos.

O próximo é que nosso código de teste reside em um blocoexpect. Abordaremos os blocos com mais detalhes em breve, mas essencialmente eles são uma maneira lógica de dividir as diferentes etapas de nossos testes.

Finalmente, percebemos que não há afirmações. Isso ocorre porque a afirmação está implícita, passando quando nossa instrução é igual atruee falhando quando é igual afalse. Novamente, cobriremos as afirmações com mais detalhes em breve.

3.2. Blocos

Às vezes, ao escrever um teste JUnit, podemos notar que não há uma maneira expressiva de dividi-lo em partes. Por exemplo, se estivéssemos seguindo o desenvolvimento orientado por comportamento, podemos acabar denotando as partesgiven when then usando comentários:

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

   // When
   int result = 2 + 2;

   // Then
   assertTrue(result == 4)
}

Spock soluciona esse problema com blocos. Blocks are a Spock native way of breaking up the phases of our test using labels. Eles nos fornecem rótulos paragiven when then e mais:

  1. Setup (com alias por dado) - aqui realizamos qualquer configuração necessária antes de um teste ser executado. Este é um bloco implícito, com o código que não está em nenhum bloco se tornando parte dele

  2. When - aqui é onde fornecemos umstimulus para o que está em teste. Em outras palavras, onde invocamos nosso método em teste

  3. Then - Aqui é onde as asserções pertencem. No Spock, elas são avaliadas como asserções booleanas simples, que serão abordadas posteriormente

  4. Expect - Esta é uma maneira de realizar nossosstimuluseassertion dentro do mesmo bloco. Dependendo do que acharmos mais expressivo, podemos ou não optar por usar esse bloco

  5. Cleanup - aqui nós eliminamos quaisquer recursos de dependência de teste que de outra forma seriam deixados para trás. Por exemplo, podemos querer remover qualquer arquivo do sistema de arquivos ou remover dados de teste gravados em um banco de dados

Vamos tentar implementar nosso teste novamente, desta vez usando todos os blocos:

def "two plus two should equal four"() {
    given:
        int left = 2
        int right = 2

    when:
        int result = left + right

    then:
        result == 4
}

Como podemos ver, os blocos ajudam nosso teste a se tornar mais legível.

3.3. Aproveitando recursos do Groovy para afirmações

Within the then and expect blocks, assertions are implicit.

Geralmente, cada instrução é avaliada e falha se não fortrue. Ao acoplar isso a vários recursos do Groovy, ele remove bem a necessidade de uma biblioteca de asserções. Vamos tentar uma afirmaçãolist 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]
}

Embora estejamos apenas abordando brevemente o Groovy neste artigo, vale a pena explicar o que está acontecendo aqui.

Primeiro, o Groovy nos fornece maneiras mais simples de criar listas. Podemos apenas declarar nossos elementos com colchetes, e internamente alist será instanciado.

Em segundo lugar, como o Groovy é dinâmico, podemos usardef, o que significa apenas que não estamos declarando um tipo para nossas variáveis.

Finalmente, no contexto de simplificação de nosso teste, o recurso mais útil demonstrado é a sobrecarga do operador. Isso significa que internamente, em vez de fazer uma comparação de referência como em Java, o métodoequals() será chamado para comparar as duas listas.

Também vale a pena demonstrar o que acontece quando nosso teste falha. Vamos quebrar e, em seguida, ver a saída para o console:

Condition not satisfied:

list == [1, 3, 4]
|    |
|    false
[2, 3, 4]
 

at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)

Enquanto tudo o que está acontecendo está chamandoequals() em duas listas, Spock é inteligente o suficiente para realizar uma análise da afirmação com falha, dando-nos informações úteis para depuração.

3.4. Afirmando exceções

Spock também fornece uma maneira expressiva de verificar exceções. No JUnit, algumas opções podem ser usar um blocotry-catch, declararexpected no início do nosso teste ou fazer uso de uma biblioteca de terceiros. As afirmações nativas de Spock vêm com uma maneira de lidar com exceções prontas para o uso:

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
}

Aqui, não tivemos que apresentar uma biblioteca adicional. Outra vantagem é que o métodothrown() afirmará o tipo da exceção, mas não interromperá a execução do teste.

4. Teste orientado a dados

4.1. O que é um teste baseado em dados?

Essencialmente,data driven testing is when we test the same behavior multiple times with different parameters and assertions. Um exemplo clássico disso seria testar uma operação matemática, como quadratura de um número. Dependendo das várias permutações de operandos, o resultado será diferente. Em Java, o termo com o qual podemos estar mais familiarizados é teste parametrizado.

4.2. Implementando um teste parametrizado em Java

Para algum contexto, vale a pena implementar um teste parametrizado usando JUnit:

@RunWith(Parameterized.class)
public class FibonacciTest {
    @Parameters
    public static Collection data() {
        return Arrays.asList(new Object[][] {
          { 1, 1 }, { 2, 4 }, { 3, 9 }
        });
    }

    private int input;

    private int expected;

    public FibonacciTest (int input, int expected) {
        this.input = input;
        this.expected = expected;
    }

    @Test
    public void test() {
        assertEquals(fExpected, Math.pow(3, 2));
    }
}

Como podemos ver, há bastante verbosidade e o código não é muito legível. Tivemos que criar uma matriz de objeto bidimensional que vive fora do teste, e até mesmo um objeto wrapper para injetar os vários valores de teste.

4.3. Usando Datatables em Spock

Uma vitória fácil para Spock, quando comparado ao JUnit, é como ele implementa testes parametrizados de maneira limpa. Novamente, em Spock, isso é conhecido comoData Driven Testing. Agora, vamos implementar o mesmo teste novamente, só que desta vez usaremos Spock comData Tables, que fornece uma maneira muito mais conveniente de realizar um teste parametrizado :

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
  }

Como podemos ver, apenas temos uma tabela de dados direta e expressiva que contém todos os nossos parâmetros.

Além disso, ele pertence ao local onde deve ser realizado, juntamente com o teste, e não há clichê. O teste é expressivo, com um nome legível por humanos, e bloco puroexpectewhere para quebrar as seções lógicas.

4.4. Quando uma tabela de dados falha

Também vale a pena ver o que acontece quando nosso teste falha:

Condition not satisfied:

Math.pow(a, b) == c
     |   |  |  |  |
     4.0 2  2  |  1
               false

Expected :1

Actual   :4.0

Mais uma vez, Spock nos fornece uma mensagem de erro muito informativa. Podemos ver exatamente qual linha do nosso Datatable causou uma falha e por quê.

5. Zombando

5.1. O que é zombaria?

Zombar é uma maneira de alterar o comportamento de uma classe com a qual nosso serviço em teste colabora. É uma maneira útil de poder testar a lógica de negócios isoladamente de suas dependências.

Um exemplo clássico disso seria substituir uma classe que faz uma chamada de rede por algo que simplesmente finge. Para uma explicação mais aprofundada, vale a pena lerthis article.

5.2. Zombando usando Spock

Spock tem sua própria estrutura de simulação, fazendo uso de conceitos interessantes trazidos para a JVM por Groovy. Primeiro, vamos instanciar umMock:

PaymentGateway paymentGateway = Mock()

Nesse caso, o tipo de nossa simulação é inferido pelo tipo de variável. Como o Groovy é uma linguagem dinâmica, também podemos fornecer um argumento de tipo, permitindo que não tenhamos de atribuir nosso mock a nenhum tipo específico:

def paymentGateway = Mock(PaymentGateway)

Agora, sempre que chamarmos um método em nossoPaymentGateway mock,, uma resposta padrão será fornecida, sem uma instância real sendo chamada:

when:
    def result = paymentGateway.makePayment(12.99)

then:
    result == false

O prazo para isso élenient mocking. Isso significa que métodos simulados que não foram definidos retornarão padrões sensíveis, em vez de gerar uma exceção. Isso ocorre por design no Spock, a fim de criar zombarias e, portanto, testa menos frágil.

5.3. Chamadas de método de stub emMocks

Também podemos configurar métodos chamados no nosso mock para responder de uma certa maneira a diferentes argumentos. Vamos tentar fazer com que nossa simulaçãoPaymentGateway retornetrue quando fizermos um pagamento de20:

given:
    paymentGateway.makePayment(20) >> true

when:
    def result = paymentGateway.makePayment(20)

then:
    result == true

O que é interessante aqui é como Spock usa a sobrecarga de operador do Groovy para fazer stub chamadas de método Com o Java, precisamos chamar métodos reais, o que significa que o código resultante é mais detalhado e potencialmente menos expressivo.

Agora, vamos tentar mais alguns tipos de stub.

Se parássemos de nos preocupar com o argumento do nosso método e sempre quiséssemos retornartrue,, poderíamos apenas usar um sublinhado:

paymentGateway.makePayment(_) >> true

Se quiséssemos alternar entre respostas diferentes, poderíamos fornecer uma lista, para a qual cada elemento será retornado em sequência:

paymentGateway.makePayment(_) >>> [true, true, false, true]

Existem mais possibilidades, e estas podem ser abordadas em um artigo futuro mais avançado sobre zombaria.

5.4. Verificação

Outra coisa que podemos querer fazer com as simulações é afirmar que vários métodos foram chamados com os parâmetros esperados. Em outras palavras, devemos verificar as interações com nossas zombarias.

Um caso de uso típico para verificação seria se um método em nosso mock tivesse um tipo de retornovoid. Nesse caso, por não haver nenhum resultado para operarmos, não há comportamento inferido para testarmos por meio do método em teste. Geralmente, se algo fosse retornado, o método em teste poderia operar nele, e é o resultado dessa operação que afirmamos.

Vamos tentar verificar se um método com um tipo de retorno nulo é chamado:

def "Should verify notify was called"() {
    given:
        def notifier = Mock(Notifier)

    when:
        notifier.notify('foo')

    then:
        1 * notifier.notify('foo')
}

Spock está aproveitando a sobrecarga do operador Groovy novamente. Ao multiplicar nossa chamada de método de zombaria por uma, estamos dizendo quantas vezes esperamos que ela tenha sido chamada.

Se nosso método não tivesse sido chamado de todo ou, alternativamente, não tivesse sido chamado quantas vezes especificamos, nosso teste falharia em fornecer uma mensagem informativa de erro do Spock. Vamos provar isso esperando que ele tenha sido chamado duas vezes:

2 * notifier.notify('foo')

Em seguida, vamos ver como é a mensagem de erro. Nós faremos isso como de costume; é bastante informativo:

Too few invocations for:

2 * notifier.notify('foo')   (1 invocation)

Assim como o stubbing, também podemos realizar uma correspondência de verificação mais flexível. Se não nos importássemos com o parâmetro do nosso método, poderíamos usar um sublinhado:

2 * notifier.notify(_)

Ou se quisermos ter certeza de que não foi chamado com um argumento específico, podemos usar o operador not:

2 * notifier.notify(!'foo')

Novamente, há mais possibilidades, que podem ser abordadas em um artigo mais avançado no futuro.

6. Conclusão

Neste artigo, demos uma olhada rápida nos testes com Spock.

Demonstramos como, aproveitando o Groovy, podemos tornar nossos testes mais expressivos do que a pilha JUnit típica. Explicamos a estrutura despecifications efeatures.

E nós mostramos como é fácil realizar testes baseados em dados e também como simulações e asserções são fáceis por meio da funcionalidade nativa do Spock.

A implementação desses exemplos pode ser encontradaover on GitHub. Este é um projeto baseado em Maven, portanto deve ser fácil de executar como está.