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.