Cucumber's skeleton project structure and API Client

Cucumber's skeleton project structure and API Client

The second part of this Guide introduces the real-life example project of Cucumber tests for end-to-end functional scripts. You’ll create the skeleton project, design the structure of packages and classes, and implement the HttpClient to trigger API calls to the backend system. Besides, we’ll add already two complete Cucumber’s Feature files using the most important Gherkin syntax’s functionalities.

> The Cucumber Java Guide
GitHub
Learn how to build end-to-end tests with Java and Cucumber, using this step-by-step tutorial with practice code examples. This guide is part of the book's extra chapters.
Part 2. The Cucumber skeleton project structure and API Client (this article)
Part 2 - Table of Contents

A real example use case of Cucumber

To put Cucumber into practice, we’ll use the same system we built in the book. This project has all the challenges you would face in a real implementation of end-to-end microservice tests with Cucumber, like Eventual Consistency (which we’ll cover in detail in Part 4).

Let’s describe from a high-level perspective what the system does, in case you didn’t read the book (it’s never too late!).

The Multiplication Challenge application invites users to solve mid-complex multiplications using mental calculation only. It’s a web page where they enter their alias and the attempt to solve the challenge. All the attempts for a given user (identified by their alias) are stored and later displayed as statistics. This application also integrates Gamification features. In case the attempt is correct, the user gets their score increased and may also win badges. See Figure 1.

Figure 1. The Multiplication Challenge Application

On the technical side, the backend functionality is implemented in two microservices: multiplication and gamification. Since this project guides the reader through a realistic example of a microservice architecture, it includes other common patterns like a Gateway service (with Spring Cloud Gateway) and a Service Discovery registry (Consul). The frontend is a very simple React application. See Figure 2. If you are interested in the technical details, check the README file in the book’s Github repository (all the book’s code sources are available online).

Figure 2. Backend's High-Level Architecture

From the end-to-end testing perspective, we’ll interact with the public REST API -the exposed endpoints by the Gateway- to simulate the user’s behavior. We won’t cover the frontend in our tests; this is a common approach in many organizations because it’s a good balance between good end-to-end coverage and simplicity.

Create a Cucumber project using the archetype

Remember: All the code in this post is available on GitHub: Book - Cucumber Tests. If you find it useful, please give it a star!

Let’s start coding. To create an empty project in Cucumber we can use the Maven archetype (make sure you have installed Maven first). See Listing 1 with the complete command. You can use the last archetype version, which you’ll find in the Cucumber’s 10-minute tutorial.

$ mvn archetype:generate                        \
   "-DarchetypeGroupId=io.cucumber"           \
   "-DarchetypeArtifactId=cucumber-archetype" \
   "-DarchetypeVersion=6.6.0"                 \
   "-DgroupId=microservices.book"             \
   "-DartifactId=cucumber-tests"              \
   "-Dpackage=microservices.book"             \
   "-Dversion=1.0.0-SNAPSHOT"                 \
   "-DinteractiveMode=false"

Listing 1. Creating a Cucumber project using the Maven archetype

That command will create a new folder called cucumber-tests.

Learn Microservices with Spring Boot - Second Edition

The archetype generates a basic pom.xml that defines dependencies with the latest versions of the cucumber-java and cucumber-junit artifacts, and it adds JUnit too. Besides, it creates a class for us to run the Cucumber features with JUnit: RunCucumberTests. See Listing 2. We described the annotations in this class already in the first part of this guide when we covered the Cucumber-JUnit integration.

package microservices.book;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty"})
public class RunCucumberTest {
}

Listing 2. The RunCucumberTest class generated by the archetype

The archetype generates a StepDefinitions class as well. See Listing 3. It even includes some useful import clauses, so we can start adding the annotated methods that map to our Gherkin (.feature) files.

package microservices.book;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import static org.junit.Assert.*;

public class StepDefinitions {
}

Listing 3. An empty StepDefinitions class, created by the archetype

Adding AssertJ, JSON deserialization, Awaitility

In this Cucumber example project, we’ll use some additional libraries:

  • AssertJ. A Java library that comes with more fluent and powerful assertions when compared to JUnit. If you want to learn more about it, save the post “BDD Unit Tests with AssertJ and BDDMockito” for later.
  • Jackson. The most popular (de)serialization implementations for Java. We’ll exchange data with the backend’s REST APIs in JSON format; this library helps us to translate JSON from/to Java objects. Check the post “Java and JSON serialization with ObjectMapper” for a detailed guide on how Jackson conversions work.
  • Awaitility. This useful library helps us write fluent code to wait for a condition to happen. It makes it very easy to create methods that poll a system until something happens, and this is exactly what we need to do in our eventually consistent system. We’ll cover this functionality in the last part of the guide.

The final version of the pom.xml file with all these dependencies and a more recent Java version is the one shown in Listing 4.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>microservices.book</groupId>
    <artifactId>cucumber-tests</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>14</java.version>
        <cucumber.version>6.6.0</cucumber.version>
        <junit.version>4.13</junit.version>
        <assertj.version>3.17.2</assertj.version>
        <jackson.version>2.11.2</jackson.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>${assertj.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.awaitility</groupId>
            <artifactId>awaitility</artifactId>
            <version>4.0.3</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Listing 4. The complete pom.xml file in the Cucumber Tests project

How to map features to Cucumber’s Gherkin syntax

As described in detail in the book (and summarized at the beginning of this post), the main functionality of our application is to allow users to solve challenges to improve their brain skills. As an engagement addition, our application gives points and badges. Actually, I don’t need to get into details about how the system’s features work because the test scripts are self-explanatory. They are an example of how we can leverage Cucumber and Gherkin to show people how a system works.

Learn Microservices with Spring Boot - Second Edition

The “Solving challenges” feature

The first Feature file we’ll add was partially included in Part 1 as a Gherkin example: the “Solving Challenges” feature. See Listing 5. We can create this file under the src/test/resources folder, which Cucumber will scan. In our case, we’ll keep a structure similar to the source code, so we’ll place it in the subfolder microservices/book.

Feature: Solve multiplication challenges
  We present users with challenges they should solve using mental calculation
  only. When they're right, we give them score and, in some cases, new badges.
  All attempts from all users are stored, so they can see their historical data.

  Scenario: Users get new attempts.
    Given a new user Mary
    When she requests a new challenge
    Then she gets a mid-complexity multiplication to solve

  Scenario: Users solve challenges, they get feedback and their stats.
    Given a new user John
    And he requests a new challenge
    When he sends the correct challenge solution
    Then his stats include 1 correct attempt

  Scenario: Users get feedback also about incorrect attempts.
  Scenario: Users get feedback also about incorrect attempts.
    Given a new user Horatio
    When he requests a new challenge
    * he sends the incorrect challenge solution
    * he sends the correct challenge solution
    Then his stats include 1 correct attempt
    * his stats include 1 incorrect attempts

Listing 5. The first feature file: solving_challenges.feature

Let’s focus on the first scenario: “Users get new attempts”. We want to check if the system retrieves a multiplication challenge for a new User. Following a “Given-When-Then” syntax, we define these steps:

  • (Given) a new user X: this sentence defines the state of the system, and simulates a user that has accessed for their first time to the web page. Note that we don’t need to know their name for this particular scenario, but we’re reusing this step in others, so it’s useful to keep it like this for consistency.
  • (When) she (or he) requests a new challenge: this is the main action made by the user. In our system, this is done automatically by the frontend when the user accesses our web page, or when the user reloads the contents.
  • (Then) she (or he) gets a mid-complexity multiplication to solve: what we want to verify. Does the system return a multiplication with two factors between the expected limits? We’ll assert this.

In Cucumber, we can reuse Step Definitions across scenarios and features. In this Gherkin file, we’re doing that with the first two steps. To make that work, we’ll make the name of the user a parameter to be passed to the Java method. Besides, we’ll define an alternative text in many of these steps, he/she, to make Cucumber a bit more inclusive while we also support a natural language.

The second and third scenarios introduce two new step patterns:

  • he/she sends the correct/incorrect challenge solution: it captures the action from the user when they send their attempt. This step should allow us to send both correct and incorrect solutions.
  • his/her stats include N correct/incorrect attempt(s): we can use this one to collect the stats of the current user and verify if they contain the expected number (N) of correct or incorrect attempts.

Rephrasing Gherkin steps to make them reusable

As explained in the first part of the guide, the feature definitions usually go over a continuous rephrasing process:

  1. First, you come up with some natural language during the session where you discuss and discover the requirements.
  2. Then, you try to find common patterns in your scenarios and rephrase them to make them match.
  3. The last rephrasing stage usually happens when you’re writing the Cucumber step definitions in source code. You may find out extra patterns or expressions that could be used if you adjust the Gherkin sentences.

For example, in my case, I wrote originally the first scenario in Listing 5 as:

  Scenario: Users get new attempts.
    When a user accesses the application
    Then they get a mid-complexity multiplication to solve

Then, while mapping the other two scenarios to Java code, I noticed that I could reuse the same steps to define a user and retrieve a new challenge while keeping this scenario readable. This rephrasing process in general allowed me to define these three test scenarios with only five different Gherkin sentences.

The “Leaderboard” feature

From a user’s perspective, the second feature of our system is the leaderboard. It shows a ranking of all users with their corresponding scores. Listing 6 describes this feature with a single scenario.

Feature: The Leaderboard shows a ranking with all the users who solved
  challenges correctly. It displays them ordered by the highest score first.

  Scenario: Users get points and badges when solving challenges, and they
  are positioned accordingly in the Leaderboard.
    Given the following solved challenges
      | user  | solved_challenges |
      | Karen | 5                 |
      | Laura | 7                 |
    Then Karen has 50 points
    * Karen has the "First time" badge
    And Laura has 70 points
    * Laura has the "First time" badge
    * Laura has the "Bronze" badge
    And Laura is above Karen in the ranking

Listing 6. The leaderboard.feature Gherkin file

Again, we can find some sentences in this file that went through a rephrasing process to be able to map them to a single step definition in code. For example, (name) has N points and (name) has the (badge) badge. To demonstrate the usage of additional features, we also added here a Gherkin’s Data Table. These structures are useful to pass lists of values to our scripts while we keep them readable. We’ll see how to process this table in Part 3 of this guide.

Next, we could implement these step definitions with the corresponding expressions in code. But, before that, let’s analyze how to structure our project and the components we need, so we can avoid duplications and create a project that can be later extended with more features easily.

Remember: All the code in this post is available on GitHub: Book - Cucumber Tests. If you find it useful, please give it a star!

How to organize the structure of a Cucumber project

In Part 1, we learned that a Cucumber project needs some basic elements:

  • The feature definitions. These are the test scripts, written in Gherkin, and packed in .feature files.
  • The JUnit entry-point. A class that makes our tests executable with JUnit, using the @RunWith annotation. In our project, this was created automatically by the archetype: RunCucumberTest.java.
  • The Step definition files. The archetype also created an example of this class, a single StepDefinitions.java.

That’s the main structure, but we’ll need more classes. To begin with, putting all the logic into the same step definition class is not a good idea – it’ll become a mess. Therefore, we need to think about how to split the logic we’ll use to run the steps.

Learn Microservices with Spring Boot - Second Edition

We’ll separate the components of the project following these guidelines:

  • A separate package for all the API communication (HTTP), and the Java classes that map the JSON requests and responses (DTOs).
  • Another main package with the classes that model the domains in our system: the Challenges, and the Leaderboard. We’ll use these classes to simulate interactions and keep the current state.
  • Finally, a third package with the Step Definition classes.

You should avoid having a Step Definition class per Cucumber feature because that makes it harder to reuse steps across different features and scenarios (see Anti-patterns). In our approach, we could reuse the Challenge class across features that need to simulate the user’s behavior. We’ll see this more in detail in Part 3.

Figure 3 shows the high-level view of our components.

Figure 3. High-level Component View

In this guide, we’ll implement all those components. When we finish the development, our project structure will look like the shown in Listing 7 (remember, you can also download the complete project from Github).

cucumber-tests
|-- README.md
|-- mvnw
|-- mvnw.cmd
|-- pom.xml
`-- src
    `-- test
        |-- java
        |   `-- microservices
        |       `-- book
        |           |-- RunCucumberTest.java
        |           `-- cucumber
        |               |-- actors
        |               |   |-- Challenge.java
        |               |   `-- Leaderboard.java
        |               |-- api
        |               |   |-- APIClient.java
        |               |   `-- dtos
        |               |       |-- challenge
        |               |       |   |-- AttemptRequestDTO.java
        |               |       |   |-- AttemptResponseDTO.java
        |               |       |   `-- ChallengeDTO.java
        |               |       |-- leaderboard
        |               |       |   `-- LeaderboardRowDTO.java
        |               |       `-- users
        |               |           `-- UserDTO.java
        |               `-- steps
        |                   |-- ChallengeStepDefinitions.java
        |                   `-- GameStepDefinitions.java
        `-- resources
            |-- cucumber.properties
            `-- microservices
                `-- book
                    |-- leaderboard.feature
                    `-- solving_challenges.feature

Listing 7. The tree structure of the final project

A plain-java API Client for Cucumber tests

To communicate with our backend from the tests, we need to interact with the REST API exposed via the Gateway, located at localhost:8000 when we run the system locally. We’ll connect to both the Challenges API and the Gamification API from our new APIClient class.

We could include as a dependency one of the multiple libraries that implement an HTTP Client in Java, but we don’t really need it because we can stick to the built-in Java’s HttpClient class, available in the JDK since version 11.

Since we need to parse and generate JSON, we’ll create a Jackson’s ObjectMapper. We’ll configure it to ignore unknown properties returned from the server. This way, we can model our DTOs (Data Transfer Objects) with only the fields we need for the tests, and we keep our code working even if the backend adds new attributes to the API. The Java representations of the API that we need to create are:

  • ChallengeDTO (see code), which includes the challenge object’s response from the server.
  • AttemptRequestDTO (see code) contains the guess from the user.
  • UserDTO (see code) holds the basic information of the user: alias and server’s identifier.
  • AttemptResponseDTO (see code) models the response from the Challenges API, with the result and the user identifier.
  • LeaderboardRowDTO (see code) represents a row in the leaderboard. The complete ranking returned by the server can be mapped to a list of these objects.

All these DTOs are plain Java objects with a basic constructor and getters, so I’m not including their code here for conciseness. Check the links in the list if you’re curious.

The APIClient class’ core logic uses the HttpClient and ObjectMapper classes to perform HTTP requests to the backend’s REST API, mapping JSON from/to Java classes where needed. See Listing 8. For now, the implementation of the Body Handlers has been omitted. We’ll get to that later.

public class APIClient {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    private static final String BACKEND_HOST = "http://localhost:8000";

    private final HttpClient httpClient;

    public APIClient() {
        this.httpClient = HttpClient.newHttpClient();
    }

    public HttpResponse<ChallengeDTO> getChallenge() throws Exception {
        var getRandom = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/challenges/random")
        ).GET().build();
        return httpClient.send(getRandom, new JsonBodyHandler<>(ChallengeDTO.class));
    }

    public HttpResponse<AttemptResponseDTO> sendAttempt(AttemptRequestDTO attempt) throws Exception {
        var sendChallenge = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/attempts"))
                .header("Content-Type", "application/json")
                .POST(ofString(OBJECT_MAPPER.writeValueAsString(attempt))).build();
        return httpClient.send(sendChallenge, new JsonBodyHandler<>(AttemptResponseDTO.class));
    }

    public HttpResponse<List<AttemptResponseDTO>> getStats(String user) throws Exception {
        var getStats = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/attempts?alias=" + user)
        ).GET().build();
        return httpClient.send(getStats, new JsonListBodyHandler<>(AttemptResponseDTO.class));
    }

    public HttpResponse<List<LeaderboardRowDTO>> getLeaderboard() throws Exception {
        var getStats = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/leaders")
        ).GET().build();
        return httpClient.send(getStats, new JsonListBodyHandler<>(LeaderboardRowDTO.class));
    }

    static class JsonBodyHandler<T> implements HttpResponse.BodyHandler<T> { ... }

    static class JsonListBodyHandler<T> implements HttpResponse.BodyHandler<List<T>> { ... }

}

Listing 8. The ApiClient class

As you see, the fluent style of the HttpClient class is straightforward to use. We create requests with:

var request = HttpRequest.newBuilder([target uri]).HTTP_METHOD([optional request body content]).build();

Then, to send the requests to the server, we call:

var response = httpClient.send(request, [response body handler]);

The main responsibility of the body handler is to read and transform the HTTP response’s payload to the class we specify. The BodyHandlers class (since JDK 11) includes some built-in body handlers, for example, to convert the response body to a String object (BodyHandlers.ofString()). Unfortunately, there are no built-in JSON converters, but they’re not hard to implement using Jackson’s ObjectMapper.

We’ll build two versions of a JSON Body Handler: one deserializes single objects and another one handles lists. See Listing 9 for the implementation of the single object deserialization.

static class JsonBodyHandler<T> implements HttpResponse.BodyHandler<T> {

    private final Class<T> clazz;

    public JsonBodyHandler(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public HttpResponse.BodySubscriber<T> apply(HttpResponse.ResponseInfo responseInfo) {
        var stringBodySubscriber = HttpResponse.BodySubscribers
                .ofString(StandardCharsets.UTF_8);

        return HttpResponse.BodySubscribers.mapping(
                stringBodySubscriber,
                (body) -> {
                    try {
                        return OBJECT_MAPPER.readValue(body, this.clazz);
                    } catch (JsonProcessingException e) {
                        throw new UncheckedIOException(e);
                    }
                });
    }
}

Listing 9. Reading JSON body contents with Java’s HttpClient and BodyHandler

We use a JDK 11’s BodySubscriber to create a subscriber that receives a String. When we receive the data, we call the ObjectMapper to deserialize it to the class that we passed to this body handler. With Java generics, we make sure that this class is reusable for all the DTO types.

The other body handler is very similar. The main difference is that we return a list of objects, which requires using a different approach with ObjectMapper and a CollectionType. Read the post “Java and JSON serialization with ObjectMapper” to learn more about deserializing lists and other collections with Jackson. See Listing 10.

static class JsonListBodyHandler<T> implements HttpResponse.BodyHandler<List<T>> {

    private final CollectionType mapCollectionType;

    public JsonListBodyHandler(Class<T> clazz) {
        this.mapCollectionType = OBJECT_MAPPER.getTypeFactory()
                .constructCollectionType(List.class, clazz);
    }

    @Override
    public HttpResponse.BodySubscriber<List<T>> apply(HttpResponse.ResponseInfo responseInfo) {
        var stringBodySubscriber = HttpResponse.BodySubscribers
                .ofString(StandardCharsets.UTF_8);

        return HttpResponse.BodySubscribers.mapping(
                stringBodySubscriber,
                (body) -> {
                    try {
                        return OBJECT_MAPPER.readValue(body, this.mapCollectionType);
                    } catch (JsonProcessingException e) {
                        throw new UncheckedIOException(e);
                    }
                });
    }
}

Listing 10. The inner class JsonListBodyHandler

Remember: All the code in this post is available on GitHub: Book - Cucumber Tests. If you find it useful, please give it a star!

After we finished the ApiClient class, we have a complete client for our application server that we can use in tests. For example, if we want to retrieve a Challenge from the server and send the correct attempt to solve it, we could write the code in Listing 11.

ApiClient apiClient = new ApiClient();
ChallengeDTO challenge = apiClient.getChallenge().getBody();
AttemptRequestDTO attemptRequest = new AttemptRequestDTO(
    challenge.getFactorA(),
    challenge.getFactorB(),
    "user-alias",
    challenge.getFactorA() * challenge.getFactorB()
);
AttemptResponseDTO attemptResponse = apiClient.sendAttempt(attemptRequest);
System.out.println("Is the challenge correct according to the server? " + 
    attemptResponse.body().isCorrect()
);

Listing 11. Code snippet demonstrating the usage of ApiClient

We’ll use the ApiClient in our step definition classes, but we won’t use it directly from there. Instead, we’ll encapsulate it within classes that also include the state of the different interactions that are available in the system: the Challenge, and the Leaderboard. We’ll learn how to accomplish this in the next part of this guide.

Learn Microservices with Spring Boot - Second Edition
This article is part of a guide:
> The Cucumber Java Guide
GitHub
Learn how to build end-to-end tests with Java and Cucumber, using this step-by-step tutorial with practice code examples. This guide is part of the book's extra chapters.
Part 2. The Cucumber skeleton project structure and API Client (this article)
Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Amsterdam, The Netherlands https://thepracticaldeveloper.com

Comments