Por que precisamos do Vulcan a bordo: revisão da estrutura do Spock

A automação de testes ajuda a monitorar constantemente a qualidade de um produto de TI, além de reduzir custos em longo prazo. Existem várias abordagens em automação, por exemplo, Behavior Driven Development (BDD), desenvolvimento através do comportamento.



Associados a essa abordagem estão pepino, estrutura de robô, comportamento e outros, que separam os scripts de execução e a implementação de cada construção. Essa separação ajuda a escrever scripts legíveis, mas é demorada e, portanto, pode ser impraticável ao escrever uma implementação.



Vejamos como você pode simplificar sua experiência com o BDD usando as ferramentas certas - por exemplo, a estrutura Spock, que combina a beleza, a conveniência dos princípios do BDD e os recursos do jUnit.







Estrutura Spock



Spock é uma estrutura de teste e especificação para aplicativos Java e Groovy. Usando a plataforma JUnit como base, esta estrutura é compatível com todos os IDEs populares (em particular, IntelliJ IDEA), várias ferramentas de construção (Ant, Gradle, Maven) e servidores de integração contínua (CI).



Conforme escrevem os desenvolvedores da estrutura, Spock "é inspirado por JUnit , RSpec , jMock , Mockito , Groovy , Scala , Vulcans e outras formas de vida fascinantes."



Neste artigo, daremos uma olhada na versão mais recente disponível, Spock Framework 2.0. Suas características: a capacidade de usar JUnit5, Java 8+, groovy 2.5 (também existe um assembly com a versão 3.0). Spock é licenciado pelo Apache 2.0 e tem uma comunidade de usuários responsiva. Os desenvolvedores da estrutura continuam a refinar e desenvolver o Spock, que já inclui muitas extensões que permitem ajustar a execução do teste. Por exemplo, uma das áreas de melhoria mais interessantes anunciadas é a adição de execução de teste paralela.



Groovy



Groovy é uma linguagem de programação orientada a objetos desenvolvida para a plataforma Java como um complemento com recursos Python, Ruby e Smalltalk. O Groovy usa uma sintaxe semelhante a Java com compilação dinâmica para bytecode JVM e funciona diretamente com outro código Java e bibliotecas. A linguagem pode ser usada em qualquer projeto Java ou como uma linguagem de script.



Os recursos do groovy incluem: digitação estática e dinâmica; sintaxe embutida para listas, arrays e expressões regulares; sobrecarregando operações. No entanto, os encerramentos no Groovy apareceram muito antes do Java.



O Groovy é adequado para o desenvolvimento de teste rápido, onde você pode usar o açúcar sintático do tipo python sem ter que se preocupar com a digitação de objetos.



Recursos do Spock Framework



Um dos principais recursos da estrutura é que o desenvolvedor tem a capacidade de escrever especificações com as características esperadas do sistema usando os princípios da abordagem BDD. Essa abordagem possibilita a composição de testes funcionais voltados para o negócio para produtos de software de alta complexidade organizacional e temática.



A especificação é uma classe bacana que estende spock.lang.Specification



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


O BOM pode conter vários campos auxiliares que são acionados para cada classe de BOM.



Usando a anotação @Shared, você pode dar acesso ao campo para classes herdadas da especificação.



abstract class PagesBaseSpec extends Specification {

    @Shared
    protected WebDriver driver


    def setup() {
        this.driver = DriverFactory.createDriver()
        driver.get("www.anywebservice.ru")
    }

    void cleanup() {
        driver.quit()
    }

}


Métodos de personalização da classe BOM:



def setupSpec() {} //     feature    
def setup() {}     //    feature 
def cleanup() {}   //    feature 
def cleanupSpec() {} //     feature   


A tabela a seguir mostra quais palavras-chave e métodos na estrutura Spock têm contrapartes JUnit.







Blocos de massa



No Spock Framework, cada fase de teste é separada em um bloco de código separado (consulte a documentação para obter um exemplo ).







Um bloco de código começa com um rótulo e termina com o início do próximo bloco de código ou o final de um teste.



O bloco fornecido é responsável por definir as condições de teste iniciais.



Bloqueia o quando , o então sempre usado junto. O bloco when contém o estimulante, estímulo do sistema, e o bloco then contém a resposta do sistema.



Nos casos em que é possível encurtar a cláusula when-then para uma única expressão, você pode usar um único bloco expect... Os exemplos a seguir serão usados ​​da documentação oficial do framework Spock:



when:
def x = Math.max(1, 2)
 
then:
x == 2


ou uma expressão



expect:
Math.max(1, 2) == 2


O bloco de limpeza é usado para liberar recursos antes da próxima iteração do teste.



given:
def file = new File("/some/path")
file.createNewFile()
 
// ...
 
cleanup:
file.delete()


A cláusula where é usada para transferir dados para teste (Data Driven Testing).



def "computing the maximum of two numbers"() {
  expect:
  Math.max(a, b) == c
 
  where:
  a << [5, 3]
  b << [1, 9]
  c << [5, 9]
}


Os tipos de transferência de dados de entrada serão discutidos abaixo.



Exemplo de implementação de teste no Spock Framework



A seguir, consideraremos abordagens para a implementação de teste da página da web para autorização do usuário no sistema usando selênio.



import helpers.DriverFactory
import org.openqa.selenium.WebDriver
import spock.lang.Shared
import spock.lang.Specification

abstract class PagesBaseSpec extends Specification {

    @Shared
    protected WebDriver driver
    
    def setup() {
        this.driver = DriverFactory.createDriver()
        driver.get("www.anywebservice.ru")
    }

    void cleanup() {
        driver.quit()
    }
}


Aqui vemos a classe base da especificação da página. No início da aula, vemos a importação das classes necessárias. A seguir está a anotação compartilhada , que permite que classes derivadas acessem o driver da web. No bloco setup () , vemos o código para inicializar o driver da web e abrir a página da web. O bloco cleanup () contém o código para encerrar o driver da web.



A seguir, vamos passar para uma visão geral da especificação da página de login do usuário.



import pages.LoginPage
import spock.lang.Issue

class LoginPageTest extends PagesBaseSpec {

    @Issue("QAA-1")
    def "QAA-1: Authorization with correct login and password"() {

        given: "Login page"
        def loginPage = new LoginPage(driver)

        and: "Correct login and password"
        def adminLogin = "adminLogin"
        def adminPassword = "adminPassword"

        when: "Log in with correct login and password"
        loginPage.login(adminLogin, adminPassword)

        then: "Authorized and moved to main page"
        driver.currentUrl == "www.anywebservice.ru/main"
    }
}


A especificação da página de autorização herda da especificação da página base. A anotação de problema especifica o ID do teste em um sistema de rastreamento externo (como Jira). Na próxima linha, vemos o nome do teste, que por convenção é definido por strings literais, o que permite que você use quaisquer caracteres no nome do teste (incluindo os russos). No bloco dado , o objeto de página da classe de página de autorização é inicializado, e o login e senha corretos para autorização no sistema são obtidos. No bloco quando , a ação de autorização é executada. O bloco then é utilizado para verificar a ação esperada, ou seja, autorização bem-sucedida e redirecionamento para a página principal do sistema.



No exemplo desta especificação, vemos a vantagem mais significativa de usar o paradigma BDD no spock - a especificação do sistema é ao mesmo tempo sua documentação. Cada teste descreve um determinado comportamento, cada etapa do teste tem sua própria descrição, que é compreensível não apenas para desenvolvedores, mas também para clientes. As descrições dos blocos podem ser apresentadas não apenas no código-fonte do teste, mas também em mensagens de diagnóstico ou relatórios sobre a operação do teste.



A estrutura fornece a capacidade de transferir vários logins e senhas para teste (parametrizar o teste).



Teste orientado a dados no Spock Framework



Teste baseado em dados = teste baseado em tabela = teste parametrizado


Para testar um cenário com vários parâmetros, você pode usar várias opções para transmiti-los.



Tabelas de dados



Vejamos alguns exemplos da documentação oficial do framework.



class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c
 
    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }
}


Cada linha da tabela é uma iteração de teste separada. Além disso, a tabela pode ser representada por uma coluna.



where:
a | _
1 | _
7 | _
0 | _


_ É um objeto stub da classe BOM.



Para uma melhor percepção visual dos parâmetros, você pode reescrever o exemplo acima da seguinte forma:



def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c
 
    where:
    a | b || c
    1 | 3 || 3
    7 | 4 || 7
    0 | 0 || 0
}


Agora podemos ver que a , b são entradas e c é o valor esperado.



Canais de dados



Em alguns casos, usar a tabela de projeto será muito complicado. Nesses casos, você pode usar o seguinte tipo de passagem de parâmetro:



...
where:
a << [1, 7, 0]
b << [3, 4, 0]
c << [3, 7, 0]


Aqui, o deslocamento para a esquerda << é um operador groovy sobrecarregado que agora serve para adicionar itens à lista.



Para cada iteração de teste, os seguintes dados serão solicitados da lista para cada variável:



1 iteração: a = 1, b = 3, c = 3;

2ª iteração: a = 7, b = 4, c = 7;

3 iteração: a = 0, b = 0, c = 0.



Além disso, os dados de entrada podem não apenas ser transmitidos explicitamente, mas também solicitados, se necessário, de várias fontes. Por exemplo, do banco de dados:



@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
 
def "maximum of two numbers"() {
  expect:
  Math.max(a, b) == c
 
  where:
  [a, b, c] << sql.rows("select a, b, c from maxdata")
}


Atribuição de Variável de Dados



...
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b


Aqui vemos a variável c calculada dinamicamente nos dados de teste.



Combinação de diferentes tipos de transferência de parâmetros



...
where:
a | _
3 | _
7 | _
0 | _
 
b << [5, 0, 0]
 
c = a > b ? a : b


Ninguém o proíbe de usar vários tipos de transferência ao mesmo tempo, se necessário.



Um exemplo de implementação de um teste parametrizado no Spock Framework



@Issue("QAA-1-parametrized")
def "QAA-1-parametrized: Authorization with correct login and password"() {

   given: "Login page"
   def loginPage = new LoginPage(driver)

   when: "Log in with correct login and password"
   loginPage.login(login, password)

   then: "Authorized and moved to main page"
   driver.currentUrl =="www.anywebservice.ru/main"

   where: "Check for different logins and passwords"
   login            | password
   "adminLogin"     | "adminPassword"
   "moderatorLogin" | "moderatorPassword"
   "userLogin"      | "userPassword"
}


Aqui vemos o já conhecido bloco where, no qual são definidas as chaves dos parâmetros (logins e senhas), que ficam armazenados no arquivo de configuração.



Devido às peculiaridades da implementação da especificação utilizada, o ciclo de configuração do driver web e seu fechamento (e consequentemente fechamento do navegador) serão realizados para cada parâmetro, o que afetará negativamente o tempo de execução do teste. Sugerimos finalizar a especificação e melhorar o tempo de execução do teste.



Um exemplo da implementação de um teste parametrizado com uma especificação modificada



Antes da revisão



abstract class PagesBaseSpec extends Specification {

    @Shared
    protected WebDriver driver


    def setup() {
        this.driver = DriverFactory.createDriver()
        driver.get("www.anywebservice.ru")
    }

    void cleanup() {
        driver.quit()
    }

}


Após revisão



import helpers.DriverFactory
import org.openqa.selenium.WebDriver
import spock.lang.Shared
import spock.lang.Specification

abstract class PagesNoRestartBaseSpec extends Specification {

    @Shared
    protected WebDriver driver

    def setupSpec() {
        this.driver = DriverFactory.createDriver()
    }

    def setup() {
        this.driver.get("www.anywebservice.ru")
    }

    def cleanup() {
        this.driver.get("www.anywebservice.ru/logout")
        this.driver.manage().deleteAllCookies();
    }

    void cleanupSpec() {
        this.driver.quit()
    }
}


Na especificação atualizada, vemos que o procedimento para a criação de um driver da web será executado apenas ao configurar a classe de especificação e fechar o navegador somente após a execução dos testes da especificação. No método setup (), vemos o mesmo código para obter o endereço da web do serviço e abri-lo em um navegador, e no método cleanup (), vamos para www.anywebservice.ru/logout para terminar de trabalhar com o serviço para o usuário atual e excluir os cookies (para testar o serviço da web atual, este procedimento é suficiente para simular um lançamento "exclusivo"). O código de teste em si não mudou.



Como resultado, com a ajuda de melhorias simples, conseguimos uma redução de pelo menos duas vezes no tempo de operação do autoteste, em comparação com a implementação inicial.



Comparação de testes para testNG, pytest, pytest-bdd



Primeiro, veremos a implementação de um teste no framework de teste testNG na linguagem de programação Java, que, como o Spock Framework, é inspirado no framework jUnit e oferece suporte a testes orientados a dados.



package javaTests;

import org.testng.Assert;
import org.testng.annotations.*;
import pages.LoginPage;


public class LoginPageTest extends BaseTest {


    @BeforeClass
    public final void setup() {
        createDriver();
        driver.get("www.anywebservice.ru");
    }

    @DataProvider(name = "userParameters")
    public final Object[][] getUserData(){
        return new Object[][] {
                {"adminLogin", "adminPassword"},
                {"moderatorLogin", "moderatorPassword"},
                {"userLogin", "userPassword"}
        };
    }

    @Test(description = "QAA-1-1: Authorization with correct login and password",
            dataProvider = "userParameters")
    public final void authorizationWithCorrectLoginAndPassword(String login, String password){
        //Login page
        LoginPage loginPage = new LoginPage(driver);

        //Log in with correct login and password
        loginPage.login(login, password);

        //Authorized and moved to main page
        Assert.assertEquals("www.anywebservice.ru/main", driver.getCurrentUrl());
    }

    @AfterMethod
    public final void cleanup() {
        driver.get("www.anywebservice.ru/logout");
        driver.manage().deleteAllCookies();
    }

    @AfterClass
    public final void tearDown() {
        driver.quit();
    }
}


Aqui podemos ver a classe de teste com todos os métodos setup (), cleanup () necessários, bem como a parametrização do teste na forma de um método getUserData () adicional com a anotação @DataProvider, que parece um pouco complicado depois do que examinamos no teste usando Spock Estrutura. Além disso, para entender o que está acontecendo no teste, foram deixados comentários semelhantes à descrição das etapas.



É importante notar que testNG, ao contrário do Spock Framework, oferece suporte à execução de testes paralelos.







A seguir, vamos passar para um teste usando a estrutura de teste pytest na linguagem de programação Python.



import pytest
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

from PageObjects.LoginPage import LoginPage


class TestLogin(object):

    @pytest.mark.parametrize("login,password", [
        pytest.param(("adminLogin", "adminPassword"), id='admin'),
        pytest.param(("moderatorLogin", "moderatorPassword"), id='moderator'),
        pytest.param(("userLogin", "userPassword"), id='user')
    ])
    def test_authorization_with_correct_login_and_password(self, login, password, driver, test_cleanup):
        # Login page
        login_page = LoginPage(driver)
        # Log in with correct login and password
        login_page.login(login, password)

        # Authorized and moved to main page
        assert expected_conditions.url_to_be("www.anywebservice.ru/main")
 
    @pytest.fixture()
    def test_cleanup(self, driver):
        yield "test"
        driver.get("www.anywebservice.ru/logout")
        driver.delete_all_cookies()


Aqui também vemos o suporte para testes orientados a dados como uma construção separada, semelhante a @DataProvider em testNG. O método para configurar o driver da web está "escondido" no dispositivo do driver. Graças à tipagem dinâmica e dispositivos pytest, o código para este teste parece mais limpo do que Java.







A seguir, vamos passar para uma visão geral do código de teste usando o plug-in pytest-bdd, que permite escrever testes na forma de arquivos de recursos do Gherkin (abordagem pura do BDD).



login.feature



Feature: Login page
  A authorization

  Scenario: Authorizations with different users
    Given Login page
    When Log in with correct login and password
    Then Authorized and moved to main page


test_login.py



import pytest
from pytest_bdd import scenario, given, when, then
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.wait import WebDriverWait

from PageObjects.LoginPage import LoginPage


@pytest.mark.parametrize("login,password", [
    pytest.param(("adminLogin", "adminPassword"), id='admin'),
    pytest.param(("moderatorLogin", "moderatorPassword"), id='moderator'),
    pytest.param(("userLogin", "userPassword"), id='user')
])
@scenario('login.feature', 'Authorizations with different users')
def test_login(login, password):
    pass


@given('Login page')
def login_page(driver):
    return LoginPage(driver)


@when('Log in with correct login and password')
def login_with_correct_login_and_password(login_page, login, password):
    login_page_object = login_page
    login_page_object.login(login, password)

@then('Authorized and moved to main page')
def authorized_and_moved_to_main_page(driver, login):
    assert expected_conditions.url_to_be("www.anywebservice.ru/main")


Uma das vantagens é que ainda é um framework pytest, que possui diversos plug-ins para diversas situações, inclusive para execução de testes em paralelo. A desvantagem é a abordagem pura do BDD, que limitará constantemente o desenvolvedor com suas próprias características. O Spock Framework torna possível escrever um código mais conciso e fácil de projetar em comparação com o pacote PyTest + pytest-bdd.







Conclusão



Neste artigo, vimos como simplificar o trabalho com o BDD usando a estrutura Spock. Resumindo, vamos destacar brevemente os principais, em nossa opinião, os prós e contras de Spock em comparação com alguns outros frameworks de teste comuns.



Prós:



  • Usar os princípios do BDD em vez de uma abordagem pura do BDD oferece mais flexibilidade ao escrever testes.
  • A especificação do teste escrito também é a documentação do sistema.
  • .
  • groovy ( , , closures ).


:



  • groovy. , , IDE , . Intellij IDEA, , , , .
  • groovy JVM -. , groovy, , . java, groovy .
  • O conjunto de extensões não é tão extenso quanto o do testNG, por exemplo. Como resultado, não há execução paralela de testes. Existem planos para adicionar essa funcionalidade, mas o momento de sua implementação é desconhecido.


Em última análise, o Spock Framework sem dúvida merece atenção, pois é adequado para resolver o complexo problema comum de automatizar cenários de negócios para produtos de software com alta complexidade organizacional e temática.



O que mais você pode ler:






All Articles