Teste da API REST com pepino

Teste da API REST com pepino

1. Visão geral

Este tutorial fornece uma introdução aCucumber, uma ferramenta comumente usada para teste de aceitação do usuário e como usá-la em testes de API REST.

Além disso, para tornar o artigo independente e independente de qualquer serviço REST externo, usaremos o WireMock, uma biblioteca de serviços da web com stubbing e mocking. Se você quiser saber mais sobre esta biblioteca, consulteintroduction to WireMock.

2. Pepino - a linguagem do pepino

Pepino é uma estrutura de teste que suporta BDD (Behavior Driven Development), permitindo que os usuários definam operações de aplicativos em texto sem formatação. Funciona com base noGherkin Domain Specific Language (DSL). Essa sintaxe simples, porém poderosa, do Gherkin permite que desenvolvedores e testadores escrevam testes complexos, mantendo-o compreensível até para usuários não técnicos.

2.1. Introdução ao Gherkin

Gherkin é uma linguagem orientada a linhas usando finais de linha, recuos e palavras-chave para definir documentos. Cada linha que não está em branco geralmente começa com uma palavra-chave Gherkin, seguida por um texto arbitrário, que geralmente é uma descrição da palavra-chave.

Toda a estrutura deve ser gravada em um arquivo com a extensãofeature para ser reconhecida pelo Cucumber.

Aqui está um exemplo simples de documento Gherkin:

Feature: A short description of the desired functionality

  Scenario: A business situation
    Given a precondition
    And another precondition
    When an event happens
    And another event happens too
    Then a testable outcome is achieved
    And something else is also completed

As subseções abaixo descrevem alguns dos elementos mais importantes em uma estrutura Gherkin.

2.2. Característica

Um arquivo Gherkin é usado para descrever um recurso de aplicativo que precisa ser testado. O arquivo contém a palavra-chaveFeature no início, seguida pelo nome do recurso na mesma linha e uma descrição opcional que pode se estender por várias linhas abaixo.

Todo o texto, exceto a palavra-chaveFeature, é ignorado pelo analisador Cucumber e incluído apenas para fins de documentação.

2.3. Cenários e etapas

Uma estrutura do Gherkin pode consistir em um ou mais cenários, reconhecidos pela palavra-chaveScenario. Um cenário é basicamente um teste que permite aos usuários validar uma capacidade do aplicativo. Ele deve descrever um contexto inicial, eventos que podem ocorrer e resultados esperados criados por esses eventos.

Essas coisas são feitas usando etapas, identificadas por uma das cinco palavras-chave:Given,When,Then,And eBut.

  • Given: esta etapa coloca o sistema em um estado bem definido antes que os usuários comecem a interagir com o aplicativo. Uma cláusulaGiven pode ser considerada uma pré-condição para o caso de uso.

  • When: uma etapaWhen é usada para descrever um evento que ocorre no aplicativo. Pode ser uma ação executada pelos usuários ou um evento acionado por outro sistema.

  • Then: esta etapa é para especificar um resultado esperado do teste. O resultado deve estar relacionado aos valores comerciais do recurso em teste.

  • AndeBut: essas palavras-chave podem ser usadas para substituir as palavras-chave da etapa acima quando houver várias etapas do mesmo tipo.

O pepino não distingue essas palavras-chave, mas elas ainda estão lá para tornar o recurso mais legível e consistente com a estrutura do BDD.

3. Implementação Cucumber-JVM

O pepino foi originalmente escrito em Ruby e foi portado para Java com a implementação do Cucumber-JVM, que é o assunto desta seção.

3.1. Dependências do Maven

Para usar o Cucumber-JVM em um projeto Maven, a seguinte dependência precisa ser incluída no POM:


    info.cukes
    cucumber-java
    1.2.4
    test

Para facilitar o teste JUnit com o Pepino, precisamos ter mais uma dependência:


    info.cukes
    cucumber-junit
    1.2.4

Como alternativa, podemos usar outro artefato para tirar proveito das expressões lambda no Java 8, que não serão abordadas neste tutorial.

3.2. Definições das etapas

Os cenários de pepino seriam inúteis se não fossem traduzidos em ações e é aí que as definições das etapas entram em cena. Basicamente, uma definição de etapa é um método Java anotado com um padrão anexado cuja tarefa é converter as etapas do Gherkin em texto sem formatação em código executável. Após analisar um documento de recurso, o Cucumber procurará definições de etapas que correspondam às etapas predefinidas do Gherkin a serem executadas.

Para deixar isso mais claro, vamos dar uma olhada na seguinte etapa:

Given I have registered a course in example

E uma definição de etapa:

@Given("I have registered a course in example")
public void verifyAccount() {
    // method implementation
}

Quando o Pepino lê a etapa especificada, ele procura definições de etapas cujos padrões de anotação correspondem ao texto Gherkin. Em nossa ilustração, o métodotestMethod é compatível e seu código é então executado, resultando na stringLet me in! impressa no console.

4. Criação e execução de testes

4.1. Gravando um arquivo de recurso

Vamos começar declarando cenários e etapas em um arquivo com o nome terminando na extensão.feature:

Feature: Testing a REST API
  Users should be able to submit GET and POST requests to a web service,
  represented by WireMock

  Scenario: Data Upload to a web service
    When users upload data on a project
    Then the server should handle it and return a success status

  Scenario: Data retrieval from a web service
    When users want to get information on the Cucumber project
    Then the requested data is returned

Agora salvamos esse arquivo em um diretório chamadoFeature, com a condição de que o diretório seja carregado no caminho de classe em tempo de execução, por exemplo, src/main/resources.

4.2. Configurando JUnit para funcionar com Cucumber

Para que o JUnit reconheça o Cucumber e leia os arquivos de recursos durante a execução, a classeCucumber deve ser declarada comoRunner. Também precisamos informar ao JUnit o local para procurar arquivos de recursos e definições de etapas.

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberTest {

}

Como você pode ver, o elementofeatures deCucumberOption localiza o arquivo de feição criado anteriormente. Outro elemento importante, chamadoglue, fornece caminhos para definições de etapas. No entanto, se as definições de caso de teste e etapa estiverem no mesmo pacote que neste tutorial, esse elemento poderá ser descartado.

4.3. Escrevendo definições de etapas

Quando o Pepino analisa etapas, ele procura métodos anotados com palavras-chave Gherkin para localizar as definições de etapas correspondentes. Neste tutorial, essas definições de etapa são definidas em uma classe dentro do mesmo pacote comCucumberTest.

A seguir, é apresentado um método que corresponde totalmente à etapa Gherkin. O método será usado para postar dados em um serviço da web REST:

@When("^users upload data on a project$")
public void usersUploadDataOnAProject() throws IOException {

}

E aqui está um método que corresponde a uma etapa do Gherkin e usa um argumento do texto, que será usado para obter informações de um serviço da Web REST:

@When("^users want to get information on the (.+) project$")
public void usersGetInformationOnAProject(String projectName) throws IOException {

}

Como você pode ver, o métodousersGetInformationOnAProject recebe um argumentoString, que é o nome do projeto. Este argumento é declarado por(.+) na anotação e aqui corresponde aCucumber no texto da etapa.

O código de trabalho para os dois métodos acima será fornecido na próxima seção.

4.4. Criação e execução de testes

Primeiro, começaremos com uma estrutura JSON para ilustrar os dados carregados no servidor por uma solicitação POST e baixados para o cliente usando um GET. Esta estrutura é salva no campojsonString, e mostrada a seguir:

{
    "testing-framework": "cucumber",
    "supported-language":
    [
        "Ruby",
        "Java",
        "Javascript",
        "PHP",
        "Python",
        "C++"
    ],

    "website": "cucumber.io"
}

Para demonstrar uma API REST, um servidor WireMock entra em jogo:

WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());

Além disso, este tutorial fará uso da API Apache HttpClient para representar o cliente usado para conectar-se ao servidor:

CloseableHttpClient httpClient = HttpClients.createDefault();

Agora, vamos prosseguir para escrever o código de teste nas definições de etapas. Faremos isso para o métodousersUploadDataOnAProject primeiro.

O servidor deve estar em execução antes que o cliente se conecte a ele:

wireMockServer.start();

Usando a API do WireMock para stub o serviço REST:

configureFor("localhost", wireMockServer.port());
stubFor(post(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json"))
  .withRequestBody(containing("testing-framework"))
  .willReturn(aResponse().withStatus(200)));

Agora, envie uma solicitação POST com o conteúdo retirado do campojsonString declarado acima para o servidor:

HttpPost request = new HttpPost("http://localhost:" + wireMockServer.port() + "/create");
StringEntity entity = new StringEntity(jsonString);
request.addHeader("content-type", "application/json");
request.setEntity(entity);
HttpResponse response = httpClient.execute(request);

O código a seguir afirma que a solicitação POST foi recebida e manipulada com êxito:

assertEquals(200, response.getStatusLine().getStatusCode());
verify(postRequestedFor(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json")));

O servidor deve parar após ser usado:

wireMockServer.stop();

O segundo método que implementaremos aqui éusersGetInformationOnAProject(String projectName). Semelhante ao primeiro teste, precisamos iniciar o servidor e, em seguida, stub o serviço REST:

wireMockServer.start();

configureFor("localhost", wireMockServer.port());
stubFor(get(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json"))
  .willReturn(aResponse().withBody(jsonString)));

Enviando uma solicitação GET e recebendo uma resposta:

HttpGet request = new HttpGet("http://localhost:" + wireMockServer.port() + "/projects/" + projectName.toLowerCase());
request.addHeader("accept", "application/json");
HttpResponse httpResponse = httpClient.execute(request);

Vamos converter a variávelhttpResponse emString usando um método auxiliar:

String responseString = convertResponseToString(httpResponse);

Aqui está a implementação desse método auxiliar de conversão:

private String convertResponseToString(HttpResponse response) throws IOException {
    InputStream responseStream = response.getEntity().getContent();
    Scanner scanner = new Scanner(responseStream, "UTF-8");
    String responseString = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return responseString;
}

O seguinte verifica todo o processo:

assertThat(responseString, containsString("\"testing-framework\": \"cucumber\""));
assertThat(responseString, containsString("\"website\": \"cucumber.io\""));
verify(getRequestedFor(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json")));

Por fim, pare o servidor como descrito anteriormente.

5. Executando recursos em paralelo

Às vezes, precisamos executar os recursos em paralelo para acelerar o processo de teste.

We can use the cucumber-jvm-parallel-plugin to create a separate runner for each feature/scenario. Em seguida, vamos configurar omaven-failsafe-plugin para executar os corredores resultantes em paralelo.

Primeiro, precisamos adicionarcucumber-jvm-parallel-plugin ao nossopom.xml:


  com.github.temyers
  cucumber-jvm-parallel-plugin
  5.0.0
  
    
      generateRunners
      generate-test-sources
      
        generateRunners
      
      
        
          com.example.rest.cucumber
        
        src/test/resources/Feature/
        SCENARIO
      
    
  

Podemos personalizar facilmente ocucumber-jvm-parallel-plugin, pois ele tem vários parâmetros. Aqui estão os que usamos:

  • glue.package: (obrigatório) nosso pacote de teste de integração

  • featuresDirectory: o caminho para o diretório contém nossos arquivos de recursos

  • parallelScheme: pode ser SCENARIO ou FEATURE, onde SCENARIO gera um runner por cenário e FEATURE gera um runner por recurso

Agora, também vamosconfigure the maven-failsafe-plugin to execute resulting runners in parallel:


    maven-failsafe-plugin
    2.19.1
    
        classes
        2
    
    
        
            
                integration-test
                verify
            
        
    

Observe que:

  • parallel: pode serclasses, methods ou ambos - no nosso casoclasses fará com que cada classe de teste seja executada em uma thread separada

  • threadCount: indica quantos threads devem ser alocados para esta execução

Em seguida, para executar os testes, podemos usar o comando:

mvn verify

Notaremos que cada cenário é executado em um thread separado.

6. Conclusão

Este tutorial abordou os conceitos básicos de pepino e como essa estrutura usa a linguagem específica do domínio Gherkin para testar uma API REST.

A implementação de todos esses exemplos e trechos de código pode ser encontrada ema GitHub project.