API Testing With Cucumber
An API represents a set of rules that enables services to communicate with each other. It is important to have a shared understanding of the API by the stakeholders. Behavior Driven Development (BDD) allows us to describe behaviors that are understandable to domain experts, testers, and developers through examples. A well-written example serves to represent a balanced view of competing concerns -
- Shared understanding. An example can be shared among API stakeholders to promote communication and understanding.
- Tests. Implement the example as an automated test to check the functionality of the API.
- Documentation. An example can be used by anyone to understand the system.
BDD is a very apt tool to test APIs. We will use Cucumber that supports BDD to test APIs developed in Java. One advantage of doing so is that the Java developer who implemented the API does not have to learn a new language to test it.
Test Automation Project
We have to set up a Java Maven Project that contains our BDD scenarios that we are going to automate. Include the following dependencies.
- Cucumber libraries to allow us to write BDD scenarios in Gherkin syntax
- Cucumber Pico container for dependency injection
- Rest Assured to test and validate REST calls
- JSON to handle JSON documents
- Apache Log4J to log requests and responses
- JUnit to allow us to run BDD scenarios as JUnit test
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-core</artifactId>
<version>6.6.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.6.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>gherkin</artifactId>
<version>15.0.2</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-jvm-deps</artifactId>
<version>1.0.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>6.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-picocontainer</artifactId>
<version>6.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-iostreams</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Project Organization
The picture shows a sample project organization. It consists of the following folders under test
.
- Context - Contains utilities to hold context to help communication among steps
- Microservice - Each microservice can have a folder to contain scenarios for API exposed by it. Within each microservice folder, create the following folders
- features - Holds feature files
- steps - Implements steps from feature files
- dev/qa/prod - Environment variables for different environments where APIs are hosted
- common - Implement steps common among features
- Testrunner - Runner for Cucumber tests
- Util - Contains utilities
Gherkin Basics
Cucumber understands Gherkin, a structured natural language syntax based upon the Given/When/Then format. Gherkin allows us to write business readable API specifications that can also be used as automated tests. It is written in feature files which are plain text files with .feature extension.
Each feature file contains one or more scenarios. Each scenario is made up of one or more steps. Each step starts with one of five keywords: Given, When, Then, And, But. Given, When, Then introduce respectively the context, action, and outcome section of a scenario. And But are conjunctions that continue the preceding section.
A sample test is shown below. A scenario should contain enough information to balance among its key features - shared understanding, test, and documentation.
@Tag
Feature: Perform full demographic verification of a person
Background:
Given caller presents a valid access token
Scenario: Perform full demographic verification with valid credentials
Given a person with full demographic details
| adhar_id | 123456789 |
| full_name | John Doe |
| dob | 31/12/1990 |
| phone_no | 999999999999 |
| email | john@doe.com |
And the match threshold is set to 1.0
When request is submitted for full demographic verification
Then verify that the HTTP response is 200
And a transaction id is returned
Each step in the feature file needs to be implemented in Java. In the step definition below, we
- Read the configuration for an environment
- Make REST call using Rest Assured
- Save the response of the call in the context for the benefit of other steps
public class Microservice1Steps {
private final World world;
private final Properties envConfig;
private RequestSpecification request;
public Microservice1Steps(World world) {
this.world = world;
this.envConfig = World.envConfig;
this.world.featureContext = World.threadLocal.get();
}
@Before
public void setUp() {
request = RequestSpecificationFactory.getInstance(world);
}
@Given("caller presents a valid access token")
public void getOAuth2Token() {
String grantType = envConfig.getProperty("microservice1-grant_type");
String clientId = envConfig.getProperty("microservice1-client_id");
String clientSecret = envConfig.getProperty("microservice1-client_secret");
String accessTokenUrl = envConfig.getProperty("microservice1-access_token_url");
String body = String.format("grant_type=%s&client_secret=%s&client_id=%s", grantType, clientSecret, clientId);
RequestSpecification request = RequestSpecificationFactory.getInstanceOAuth2();
Response response = request
.accept(ContentType.URLENC)
.body(body)
.post(accessTokenUrl);
String responseString = response.then().extract().asString();
String accessToken = new JSONObject(responseString).getString("access_token");
world.scenarioContext.put("accessToken", accessToken);
}
}
A set of scenarios may be run as a JUnit test:
mvn test -Denv=local -Dcucumber.filter.tags="@Tag"
Tag selects a set of tests to run and -Denv
selects the environment to run the tests on.
The complete project is available on my GitHub.
Conclusion
In this article, we learned why BDD is a natural fit for testing API as it enhances shared understanding, tests, and documentation of the API. For Java developers, a Maven project with Cucumber libraries is easy to set up to add scenarios and test them.