
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: March 17, 2024
This tutorial gives an introduction to Cucumber, a commonly used tool for user acceptance testing, and how to use it in REST API tests.
In addition, to make the article self-contained and independent of any external REST services, we will use WireMock, a stubbing and mocking web service library. If you want to know more about this library, please refer to the introduction to WireMock.
Cucumber is a testing framework that supports Behavior Driven Development (BDD), allowing users to define application operations in plain text. It works based on the Gherkin Domain Specific Language (DSL). This simple but powerful syntax of Gherkin lets developers and testers write complex tests while keeping it comprehensible to even non-technical users.
Gherkin is a line-oriented language using line endings, indentations and keywords to define documents. Each non-blank line usually starts with a Gherkin keyword, followed by an arbitrary text, which is usually a description of the keyword.
The whole structure must be written into a file with the feature extension to be recognized by Cucumber.
Here is a simple Gherkin document example:
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
In the following sections, we’ll describe a couple of the most important elements in a Gherkin structure.
We use a Gherkin file to describe an application feature that needs to be tested. The file contains the Feature keyword at the very beginning, followed up by the feature name on the same line and an optional description that may span multiple lines underneath.
Cucumber parser skips all the text, except for the Feature keyword, and includes it for the purpose of documentation only.
A Gherkin structure may consist of one or more scenarios, recognized by the Scenario keyword. A scenario is basically a test allowing users to validate a capability of the application. It should describe an initial context, events that may happen and expected outcomes created by those events.
These things are done using steps, identified by one of the five keywords: Given, When, Then, And, and But.
Cucumber does not actually distinguish these keywords, however they are still there to make the feature more readable and consistent with the BDD structure.
Cucumber was originally written in Ruby and has been ported into Java with Cucumber-JVM implementation, which is the subject of this section.
In order to make use of Cucumber-JVM in a Maven project, the following dependency needs to be included in the POM:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.8.0</version>
<scope>test</scope>
</dependency>
To facilitate JUnit testing with Cucumber, we need to have one more dependency:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>6.8.0</version>
</dependency>
Alternatively, we can use another artifact to take advantage of lambda expressions in Java 8, which won’t be covered in this tutorial.
Gherkin scenarios would be useless if they were not translated into actions and this is where step definitions come into play. Basically, a step definition is an annotated Java method with an attached pattern whose job is to convert Gherkin steps in plain text to executable code. After parsing a feature document, Cucumber will search for step definitions that match predefined Gherkin steps to execute.
In order to make it clearer, let’s take a look at the following step:
Given I have registered a course in Baeldung
And a step definition:
@Given("I have registered a course in Baeldung")
public void verifyAccount() {
// method implementation
}
When Cucumber reads the given step, it will be looking for step definitions whose annotating patterns match the Gherkin text.
Let’s start with declaring scenarios and steps in a file with the name ending in the .feature extension:
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
We now save this file in a directory named Feature, on the condition that the directory will be loaded into the classpath at runtime, e.g. src/main/resources.
In order for JUnit to be aware of Cucumber and read feature files when running, the Cucumber class must be declared as the Runner. We also need to tell JUnit the place to search for feature files and step definitions.
@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberIntegrationTest {
}
As you can see, the features element of CucumberOption locates the feature file created before. Another important element, called glue, provides paths to step definitions. However, if the test case and step definitions are in the same package as in this tutorial, that element may be dropped.
When Cucumber parses steps, it will search for methods annotated with Gherkin keywords to locate the matching step definitions.
A step definition’s expression can either be a Regular Expression or a Cucumber Expression. In this tutorial, we’ll use Cucumber Expressions.
The following is a method that fully matches a Gherkin step. The method will be used to post data to a REST web service:
@When("users upload data on a project")
public void usersUploadDataOnAProject() throws IOException {
}
And here is a method matching a Gherkin step and takes an argument from the text, which will be used to get information from a REST web service:
@When("users want to get information on the {string} project")
public void usersGetInformationOnAProject(String projectName) throws IOException {
}
As you can see, the usersGetInformationOnAProject method takes a String argument, which is the project name. This argument is declared by {string} in the annotation and over here it corresponds to Cucumber in the step text.
Alternatively, we could use a regular expression:
@When("^users want to get information on the '(.+)' project$")
public void usersGetInformationOnAProject(String projectName) throws IOException {
}
Note, the ‘^’ and ‘$’ which indicate the start and end of the regex accordingly. Whereas ‘(.+)’ corresponds to the String parameter.
We’ll provide the working code for both of the above methods in the next section.
First, we will begin with a JSON structure to illustrate the data uploaded to the server by a POST request, and downloaded to the client using a GET. This structure is saved in the jsonString field, and shown below:
{
"testing-framework": "cucumber",
"supported-language":
[
"Ruby",
"Java",
"Javascript",
"PHP",
"Python",
"C++"
],
"website": "cucumber.io"
}
To demonstrate a REST API, we use a WireMock server:
WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());
In addition, we’ll use Apache HttpClient API to represent the client used to connect to the server:
CloseableHttpClient httpClient = HttpClients.createDefault();
Now, let’s move on to writing testing code within step definitions. We will do this for the usersUploadDataOnAProject method first.
The server should be running before the client connects to it:
wireMockServer.start();
Using the WireMock API to stub the REST service:
configureFor("localhost", wireMockServer.port());
stubFor(post(urlEqualTo("/create"))
.withHeader("content-type", equalTo("application/json"))
.withRequestBody(containing("testing-framework"))
.willReturn(aResponse().withStatus(200)));
Now, send a POST request with the content taken from the jsonString field declared above to the server:
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);
The following code asserts that the POST request has been successfully received and handled:
assertEquals(200, response.getStatusLine().getStatusCode());
verify(postRequestedFor(urlEqualTo("/create"))
.withHeader("content-type", equalTo("application/json")));
The server should stop after being used:
wireMockServer.stop();
The second method we will implement herein is usersGetInformationOnAProject(String projectName). Similar to the first test, we need to start the server and then stub the REST service:
wireMockServer.start();
configureFor("localhost", wireMockServer.port());
stubFor(get(urlEqualTo("/projects/cucumber"))
.withHeader("accept", equalTo("application/json"))
.willReturn(aResponse().withBody(jsonString)));
Submitting a GET request and receiving a response:
HttpGet request = new HttpGet("http://localhost:" + wireMockServer.port() + "/projects/" + projectName.toLowerCase());
request.addHeader("accept", "application/json");
HttpResponse httpResponse = httpClient.execute(request);
We will convert the httpResponse variable to a String using a helper method:
String responseString = convertResponseToString(httpResponse);
Here is the implementation of that conversion helper method:
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;
}
The following verifies the whole process:
assertThat(responseString, containsString("\"testing-framework\": \"cucumber\""));
assertThat(responseString, containsString("\"website\": \"cucumber.io\""));
verify(getRequestedFor(urlEqualTo("/projects/cucumber"))
.withHeader("accept", equalTo("application/json")));
Finally, stop the server as described before.
Cucumber-JVM natively supports parallel test execution across multiple threads. We’ll use JUnit together with Maven Failsafe plugin to execute the runners. Alternatively, we could use Maven Surefire.
JUnit runs the feature files in parallel rather than scenarios, which means all the scenarios in a feature file will be executed by the same thread.
Let’s now add the plugin configuration:
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<includes>
<include>CucumberIntegrationTest.java</include>
</includes>
<parallel>methods</parallel>
<threadCount>2</threadCount>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
Note that:
That’s all we need to do to run the Cucumber features in parallel.
In this tutorial, we covered the basics of Cucumber and how this framework uses the Gherkin domain-specific language for testing a REST API.