e2e-test-runner is a ready-to-use E2E test runner that leverages Cypress and Cucumber.
It provides a collection of pre-implemented reusable steps to simplify writing and maintaining end-to-end tests for your applications.
- π§ͺ Predefined Cucumber steps for HTTP requests and assertions
- π§° Built-in context system with template rendering (via Nunjucks)
- βοΈ Compatible with Cypress and Cucumber ecosystem
- π Run tests via Node or Docker
You can pull and run the prebuilt image directly from Docker Hub:
docker pull vincentmoittie/e2e-test-runner:latest
git clone https://github.com/zorin95670/e2e-test-runner e2e
Make sure to include e2e
in your .gitignore
if it's only for local use.
git submodule add https://github.com/zorin95670/e2e-test-runner e2e
Then install dependencies:
cd e2e
npm install
The test runner requires the environment variable CYPRESS_FEATURES_PATH
.
- It must point to the directory containing your
.feature
files. - The path should be relative to the current working directory (i.e., where
npm run start
is executed). - For example, in a Java project structure, if the test runner is executed from the
e2e
folder, the correct value would typically be:
CYPRESS_FEATURES_PATH=../src/test/resources/features
You must define this variable in a .env
file or inject it at runtime.
To run the test runner locally and load environment variables from a .env
file:
- Install
dotenv-cli
globally:
npm install -g dotenv-cli
- Run the test runner:
dotenv -e ../.env -- npm run start
# With ui
dotenv -e ../.env -- npm run start:ui
To run the test runner in a Docker container, make sure to:
- Provide access to the
.env
file. - Set the timezone (e.g.,
Europe/Paris
) using theTZ
environment variable. - Connect the container to the appropriate Docker network (e.g., to communicate with your application under test).
Example:
docker run --rm \
--env-file .env \
--env TZ=Europe/Paris \
--network my-app-network \
-v "$(pwd)/src/test/resources/features":/app/src/test/resources/features \
vincentmoittie:e2e-test-runner:latest
Or one-line version:
docker run --rm --env-file .env --env TZ=Europe/Paris --network my-app-network -v "$(pwd)/src/test/resources/features":/app/src/test/resources/features vincentmoittie:e2e-test-runner:latest
Replace
my-app-network
with the name of the Docker network your application is running on.
To run the test runner in a Docker container, make sure to:
- Mount the project directory.
- Provide access to the
.env
file. - Set the timezone (e.g.,
Europe/Paris
) using theTZ
environment variable. - Connect the container to the appropriate Docker network (e.g., to communicate with your application under test).
Example:
docker build e2e -t e2e-test-runner
docker run --rm \
--env-file .env \
--env TZ=Europe/Paris \
--network my-app-network \
-v "$(pwd)/src/test/resources/features":/app/src/test/resources/features \
e2e-test-runner
Or one-line version:
docker run --rm --env-file .env --env TZ=Europe/Paris --network my-app-network -v "$(pwd)/src/test/resources/features":/app/src/test/resources/features e2e-test-runner
Replace
my-app-network
with the name of the Docker network your application is running on.
The test runner supports writing tests in Gherkin using .feature
files.
Feature: Basic HTTP check
Scenario: GET Google homepage
When I request "https://www.google.com" with method "GET"
Then I expect status code is 200
You can use Nunjucks templating syntax inside step values. This allows dynamic content, especially useful when injecting data from environment variables or shared context.
Given I set http header "Authorization" with "Bearer {{ ctx.AUTH_TOKEN }}"
A custom json
filter has been added to Nunjucks to properly serialize objects into formatted JSON strings.
Then I log "{{ response.body | json }}"
This is useful for debugging or displaying formatted JSON output directly from the response.
This test runner provides a built-in context system to share data between steps. It is designed to simplify dynamic value handling, templating, and state tracking during test execution.
Key | Description |
---|---|
env |
Contains all environment variables. Useful for configuration and templating. |
ctx |
A customizable object used to store any custom data needed during the test flow. Reset before each scenario. |
response |
Stores the last HTTP response. Reset before each scenario. |
httpHeaders |
Stores all HTTP headers set via step definitions. Reset before each scenario. |
Example Usage
You can also use the context in templated strings (via Nunjucks):
When I request "{{ env.API_URL }}/users/{{ ctx.userId }}" with method "GET"
Certain steps allow you to specify a type
to interpret or convert values properly.
The following types are supported:
Type | Description |
---|---|
string |
Default type. Values are kept as-is. |
integer |
Converts the value to a JavaScript integer (e.g., "42" β 42 ). |
float |
Converts the value to a JavaScript float (e.g., "3.14" β 3.14 ). |
boolean |
Converts the value to a boolean ("true" β true , "false" β false ). |
json |
Parses the value as a JSON object (e.g., "{ \"foo\": 123 }" β { foo: 123 } ). |
π These types are used to ensure that context variables, assertions, and request parameters behave as expected during test execution.
In some steps like:
When I request "<url>" with method "<method>" with query parameters
You can define a DataTable
in your Gherkin file to pass query parameters dynamically.
The table must include at least key
and value
, and may optionally include a type
column:
When I request "/api/search" with method "GET" with query parameters
| key | value | type |
| q | banana | string |
| limit | 10 | integer |
| exact | true | boolean |
Column | Required | Description |
---|---|---|
key |
β | The name of the query parameter. |
value |
β | The value to assign to the key. |
type |
β | Optional type conversion (see supported types above). Default: string |
β
The table is evaluated with Nunjucks, so you can dynamically reference values like {{ ctx.foo }}
.
You can use tags to control which scenarios or features to run during test execution:
Use @skip
above a scenario to skip it.
Feature: User login
@skip
Scenario: Valid credentials
Given I visit the login page
When I enter valid credentials
Then I should be redirected to the dashboard
Use @only
above a scenario to run only that one. All others will be ignored.
Feature: User login
@only
Scenario: Invalid credentials
Given I visit the login page
When I enter invalid credentials
Then I should see an error message
Use @skip
above the Feature:
line to skip all scenarios in that file.
@skip
Feature: Password recovery
Scenario: Request password reset
Given I visit the forgot password page
When I enter my email
Then I should receive a reset email
- Log a value
Then I log "<value>"
# Example:
Then I log "{{ response.status }}"
- Wait a duration
Then I wait <seconds>s
#Example:
Then I wait 2s
- Store a value in context
Then I store "<key>" as "<value>" in context
# Example:
Then I store "userId" as "{{ response.body.id }}" in context
- Store a raw value in context
Then I store as "<value>":
"""
My data
"""
# Example:
Then I store as "data":
"""
My data
"""
- Basic request
When I request "<url>" with method "<method>"
# Example:
When I request "https://api.example.com/items" with method "GET"
- Request with query parameters
When I request "<url>" with method "<method>" with query parameters
| key | value | type |
| userId | 123 | integer |
# Example:
When I request "https://api.example.com/users" with method "GET" with query parameters
| key | value | type |
| userId | 123 | integer |
- Request with raw body
When I request "<url>" with method "<method>" with body:
"""
{ "name": "John" }
"""
# Example:
When I request "https://api.example.com/users" with method "POST" with body:
"""
{ "name": "John" }
"""
- Set HTTP header
Given I set http header "<key>" with "<value>"
# Example:
Given I set http header "Authorization" with "Bearer abc123"
- Check status code
Then I expect status code is <code>
# Example:
Then I expect status code is 200
- Compare templated values
Then I expect "<value>" is empty
Then I expect "<value>" is not empty
Then I expect "<value>" is "<expected>"
Then I expect "<value>" is not "<expected>"
Then I expect "<value>" contains "<expected>"
Then I expect "<value>" not contains "<expected>"
# Example:
Then I expect "{{ response.body }}" is empty
Then I expect "{{ response.body }}" is not empty
Then I expect "{{ response.status }}" is "200"
Then I expect "{{ response.status }}" is not "200"
Then I expect "{{ response.body.name }}" contains "test"
Then I expect "{{ response.body.name }}" not contains "test"
- Compare templated values with type
Then I expect "<value>" is "<expected>" as "<type>"
# Example:
Then I expect "{{ response.status }}" is "200" as "integer"
- Check length of value
Then I expect "<value>" to have length <number>
# Example:
Then I expect "{{ response.body.users }}" to have length 5
- Check length with type
Then I expect "<value>" as "<type>" to have length <number>
# Example:
Then I expect "{{ response.body.users }}" as "json" to have length 5
- One resource equals to a value
Then I expect one resource of "<value>" equals to "<expected>"
# Example:
Then I expect one resource of "{{ response.body.users }}" equals to "John"
- One resource equals with type
Then I expect one resource of "<value>" equals to "<expected>" as "<type>"
# Example:
Then I expect one resource of "{{ response.body.users }}" equals to "123" as "integer"
- Resource field match expected
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>"
# Example:
Then I expect one resource of "{{ response.body.users }}" contains "name" equals to "Alice"
- Resource field match expected with type
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>" as "<type>"
# Example:
Then I expect one resource of "{{ response.body.users }}" contains "age" equals to "30" as "integer"
- Set a value in LocalStorage
Given I set in localstorage field "<key>" with "<value>"
# Example:
Given I set in localstorage field "token" with "abc123"
- Assert a value in LocalStorage
Then I expect localstorage field "<key>" is "<expected>"
# Example:
Then I expect localstorage field "token" is "abc123"
- Delete a value from LocalStorage
Then I delete "<key>" in localstorage
# Example:
Then I delete "token" in localstorage
- Copy LocalStorage field to context
Then I set localstorage field "<localStorageField>" to context field "<contextField>"
# Example:
Then I set localstorage field "token" to context field "userToken"
- Copy LocalStorage field to context with type conversion
Then I set localstorage field "<localStorageField>" to context field "<contextField>" as "<type>"
# Example:
Then I set localstorage field "isAdmin" to context field "adminFlag" as "boolean"
- Visit a URL
Given I visit "<url>"
# Example:
Given I visit "{{ env.baseUrl }}/login"
- Reload a specific URL
Given I reload to "<url>"
# Example:
Given I reload to "{{ env.baseUrl }}/dashboard"
- Expect current URL is exactly
Then I expect current url is "<url>"
# Example:
Then I expect current url is "{{ env.baseUrl }}/home"
- Expect current URL contains a string
Then I expect current url contains "<url>"
# Example:
Then I expect current url contains "/home"
- Expect current URL matches a pattern
Then I expect current url matches "<regex>"
# Example:
Then I expect current url matches ".*\\/profile\\/\\d+$"
- Expect current URL is no longer a specific URL
Then I expect the current URL no longer is "<url>"
# Example:
Then I expect the current URL no longer is "{{ env.baseUrl }}/splash"
- Expect current URL no longer contains a string
Then I expect the current URL no longer contains "<text>"
# Example:
Then I expect the current URL no longer contains "/splash"
- Expect current URL no longer matches a pattern
Then I expect the current URL no longer matches "<regex>"
# Example:
Then I expect the current URL no longer matches ".*\\/splash$"
- Click on an element
When I click on "<selector>"
# Example:
When I click on "#submit-button"
- Force click on an element (ignores visibility)
When I force click on "<selector>"
# Example:
When I force click on ".dropdown-toggle"
- Double-click on an element
When I double click on "<selector>"
# Example:
When I double click on "#editable-cell"
- Scroll to a position inside an element
When I scroll to "<position>" into "<selector>"
# Example:
When I scroll to "bottom" into ".scrollable-container"
- Hover an element to make it visible
When I hover "<selector>" to make it visible
# Example:
When I hover ".tooltip-trigger" to make it visible
- Drag an element onto another
When I drag "<originSelector>" onto "<destinationSelector>"
# Example:
When I drag "#item" onto "#dropzone"
- Drag an element by a specific offset
When I drag "<selector>" of <x>,<y>
# Example:
When I drag "#slider" of 50,0
- Move an element by offset
When I move "<selector>" of <x>,<y>
# Example:
When I move "#box" of 20,30
- Select an option inside a dropdown or menu
When I select "<option>" in "<selector>"
# Example:
When I select ".option-2" in ".dropdown-menu"
- Expect element to exist
Then I expect the HTML element "<selector>" exists
- Expect element to not exist
Then I expect the HTML element "<selector>" not exists
- Expect element to be visible
Then I expect the HTML element "<selector>" to be visible
- Expect element to be hidden
Then I expect the HTML element "<selector>" to be hidden
- Expect element to be disabled
Then I expect the HTML element "<selector>" to be disabled
- Expect element to be enabled
Then I expect the HTML element "<selector>" to be enabled
- Expect element to have exact width
Then I expect the HTML element "<selector>" width is <width>
# Example:
Then I expect the HTML element "#image" width is 200
- Expect element to have exact height
Then I expect the HTML element "<selector>" height is <height>
# Example:
Then I expect the HTML element "#container" height is 400
- Expect element to be at a specific position
Then I expect the HTML element "<selector>" to be at position <x>,<y>
# Example:
Then I expect the HTML element "#popup" to be at position 100,200
- Expect element to have a specific attribute and value
Then I expect the HTML element "<selector>" to have attribute "<attribute>" with value "<value>"
# Example:
Then I expect the HTML element "#logo" to have attribute "alt" with value "Company Logo"
- Expect element to contain specific text
Then I expect the HTML element "<selector>" contains "<text>"
# Example:
Then I expect the HTML element "#message" contains "Welcome!"
- Expect element to not contain specific text
Then I expect the HTML element "<selector>" not contains "<text>"
# Example:
Then I expect the HTML element "#message" not contains "Welcome!"
- Expect element to have a specific value
Then I expect the HTML element "<selector>" to have value "<value>"
# Example:
Then I expect the HTML element "#username" to have value "john.doe"
- Expect element to appear a certain number of times
Then I expect the HTML element "<selector>" appear <count> time(s) on screen
# Example:
Then I expect the HTML element ".card" appear 3 time(s) on screen
- Expect element is checked
Then I expect the HTML element "<selector>" is checked
# Example:
Then I expect the HTML element ".checkbox" is checked
- Expect element is not checked
Then I expect the HTML element "<selector>" is not checked
# Example:
Then I expect the HTML element ".checkbox" is not checked
- Clear the text inside an element
Then I clear the text in the HTML element "<selector>"
# Example:
Then I clear the text in the HTML element "#search-box"
- Set a specific text inside an element
Then I set the text "<text>" in the HTML element "<selector>"
# Example:
Then I set the text "admin" in the HTML element "#username"
- Set viewport size (useful for responsive tests)
Given I set the viewport size to <width> px by <height> px
# Example:
Given I set the viewport size to 1280 px by 720 px
- Setup Kafka client with clientId and broker
Given I setup kafka with clientId "<clientId>" and broker "<broker>"
# Example:
Given I setup kafka with clientId "my-client" and broker "localhost:9092"
- Setup Kafka producer
Given I setup kafka producer
# Example:
Given I setup kafka producer
- Setup Kafka consumer with groupId
Given I setup kafka consumer with groupId "<groupId>"
# Example:
Given I setup kafka consumer with groupId "test-group"
- Listen for Kafka messages on a topic
Given I listen for Kafka messages on the topic "<topic>"
# Example:
Given I listen for Kafka messages on the topic "orders"
- Send kafka message
When I send a Kafka message on the topic "<topic>" with body "<body>"
# Example:
When I send a Kafka message on the topic "orders" with body "my data"
- Send kafka message with raw value
When I send a Kafka message on the topic "<topic>" with body:
"""
{ "name": "John" }
"""
# Example:
When I send a Kafka message on the topic "orders" with body:
"""
{ "name": "John" }
"""
- Expect a number of messages received on a Kafka topic
Then I expect {int} message(s) received on Kafka topic "<topic>"
# Example:
Then I expect 3 message(s) received on Kafka topic "orders"
- Expect a message on a Kafka topic to be equal to a string
Then I expect a message on Kafka topic "<topic>" equals to "<value>"
# Example:
Then I expect a message on Kafka topic "orders" equals to "{\"orderId\":123}"
- Expect a message on a Kafka topic to be equal to a provided type
Then I expect a message on Kafka topic "<topic>" equals to "<value>" as "<type>"
# Example:
Then I expect a message on Kafka topic "orders" equals to "{\"orderId\":123}" as "json"
- Expect a message on a Kafka topic to be equal to a multi-line string
Then I expect a message on Kafka topic "<topic>" equals to:
"""
{
"orderId": 123,
"status": "shipped"
}
"""
# Example:
Then I expect a message on Kafka topic "orders" equals to:
"""
{
"orderId": 123,
"status": "shipped"
}
"""
- Expect a message on a Kafka topic contains a substring
Then I expect a message on Kafka topic "<topic>" contains "<value>"
# Example:
Then I expect a message on Kafka topic "orders" contains "shipped"
- Expect a message on a Kafka topic contains a multi-line string
Then I expect a message on Kafka topic "<topic>" contains:
"""
{
"status": "shipped"
}
"""
# Example:
Then I expect a message on Kafka topic "orders" contains:
"""
{
"status": "shipped"
}
"""
- Expect a message on a Kafka topic matches a regex
Then I expect a message on Kafka topic "<topic>" matches regex "<regex>"
# Example:
Then I expect a message on Kafka topic "orders" matches regex "^\\{.*\"status\":\\s*\"shipped\".*\\}$"
- Log kafka messages
Then I log kafka messages
- Setup ldap client with url, bind dn and password
Given I setup Ldap with url "<url>" bind dn "<bindDn>" and password "<password>"
# Example:
Given I setup Ldap with url "ldap://localhost:389" bind dn "cn=admin,dc=company,dc=com" and password "my_password"
- Search ldap results with base Dn, filter and attributes
Given I search ldap results on base dn "<baseDn>" with filter "<filter>" and attributes "<attributes>"
# Example:
Given I search ldap results on base dn "ou=users,dc=company,dc=com" with filter "[email protected]" and attributes "cn sn mail"
- Expect search results to a given length
Given I expect "<expectedLength>" ldap results
# Example:
Given I expect 2 ldap results
- Delete all search results previously found
Given I delete all ldap results
# Example:
Given I delete all ldap results
- Add a new ldap entry with dn and with raw attributes
Given I add a ldap entry with dn "<dn>" and with attributes:
# Example:
Given I add a ldap entry with dn "uid=c1aa97e9-fdff-4b81-b468-0cb41be3e550,dc=company,dc=com" and with attributes:
"""
{
"objectClass": "top",
"objectClass": "person",
"objectClass": "organizationalPerson",
"objectClass": "inetOrgPerson",
"uid": "c1aa97e9-fdff-4b81-b468-0cb41be3e550",
"cn": "John Doe",
"sn": "Doe",
"mail": "[email protected]",
"givenName": "John"
}
"""
- Add a new ldap entry with dn and with attributes
Given I add a ldap entry with dn "<dn>" and with attributes <attributes>
# Example:
Given I add a ldap entry with dn "uid=c1aa97e9-fdff-4b81-b468-0cb41be3e550,dc=company,dc=com" and with attributes "{ ... }"
# π§ Context Utilities
Then I log "<value>"
Then I wait <seconds>s
Then I store "<key>" as "<value>" in context
Then I store as "<value>":
"""
My data
"""
# π HTTP Requests
Given I set http header "<key>" with "<value>"
When I request "<url>" with method "<method>"
When I request "<url>" with method "<method>" with query parameters
| key | value | type |
| userId | 123 | integer |
When I request "<url>" with method "<method>" with body:
"""
{ "name": "John" }
"""
# π₯ Response Assertions
Then I expect status code is <code>
Then I expect "<value>" is empty
Then I expect "<value>" is not empty
Then I expect "<value>" is "<expected>"
Then I expect "<value>" is not "<expected>"
Then I expect "<value>" is "<expected>" as "<type>"
Then I expect "<value>" to have length <number>
Then I expect "<value>" as "<type>" to have length <number>
Then I expect "<value>" contains "<expected>"
Then I expect "<value>" not contains "<expected>"
# π Resource Assertions
Then I expect one resource of "<value>" equals to "<expected>"
Then I expect one resource of "<value>" equals to "<expected>" as "<type>"
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>"
Then I expect one resource of "<value>" contains "<field>" equals to "<expected>" as "<type>"
# π Manipulate localStorage
Given I set in localstorage field "<key>" with "<value>"
Then I expect localstorage field "<key>" is "<expected>"
Then I delete "<key>" in localstorage
Then I set localstorage field "<localStorageField>" to context field "<contextField>"
Then I set localstorage field "<localStorageField>" to context field "<contextField>" as "<type>"
# π URL Navigation & Assertions
Given I visit "<url>"
Given I reload to "<url>"
Then I expect current url is "<url>"
Then I expect current url contains "<url>"
Then I expect current url matches "<regex>"
Then I expect the current URL no longer is "<url>"
Then I expect the current URL no longer contains "<text>"
Then I expect the current URL no longer matches "<regex>"
# π±οΈ HTML Element Interactions
When I click on "<selector>"
When I force click on "<selector>"
When I double click on "<selector>"
When I scroll to "<position>" into "<selector>"
When I hover "<selector>" to make it visible
When I drag "<originSelector>" onto "<destinationSelector>"
When I drag "<selector>" of <x>,<y>
When I move "<selector>" of <x>,<y>
When I select "<option>" in "<selector>"
# ποΈ HTML Element Assertions
Then I expect the HTML element "<selector>" exists
Then I expect the HTML element "<selector>" not exists
Then I expect the HTML element "<selector>" to be visible
Then I expect the HTML element "<selector>" to be hidde
Then I expect the HTML element "<selector>" to be disabled
Then I expect the HTML element "<selector>" to be enabled
Then I expect the HTML element "<selector>" is checked
Then I expect the HTML element "<selector>" is not checked
Then I expect the HTML element "<selector>" width is <width>
Then I expect the HTML element "<selector>" height is <height>
Then I expect the HTML element "<selector>" to be at position <x>,<y>
Then I expect the HTML element "<selector>" to have attribute "<attribute>" with value "<value>"
Then I expect the HTML element "<selector>" contains "<text>"
Then I expect the HTML element "<selector>" not contains "<text>"
Then I expect the HTML element "<selector>" to have value "<value>"
Then I expect the HTML element "<selector>" appear <count> time(s) on screen
# βοΈ HTML Element Text Manipulation
Then I clear the text in the HTML element "<selector>"
Then I set the text "<text>" in the HTML element "<selector>"
# π Viewport Configuration
Given I set the viewport size to <width> px by <height> px
# β Kafka Messaging Steps
Given I setup kafka with clientId "<clientId>" and broker "<broker>"
Given I setup kafka producer
Given I setup kafka consumer with groupId "<groupId>"
Given I listen for Kafka messages on the topic "<topic>"
When I send a Kafka message on the topic "<topic>" with body "<body>"
When I send a Kafka message on the topic "<topic>" with body:
"""
{ "name": "John" }
"""
Then I expect <count> message(s) received on Kafka topic "<topic>"
Then I expect a message on Kafka topic "<topic>" equals to "<value>"
Then I expect a message on Kafka topic "<topic>" equals to "<value>" as "<type>"
Then I expect a message on Kafka topic "<topic>" equals to:
"""
{
"orderId": 123,
"status": "shipped"
}
"""
Then I expect a message on Kafka topic "<topic>" contains "<value>"
Then I expect a message on Kafka topic "<topic>" contains:
"""
{
"status": "shipped"
}
"""
Then I expect a message on Kafka topic "<topic>" matches regex "<regex>"
Then I log kafka messages
If you need a step that doesn't exist yet, there are two options:
-
π¬ Open an issue: Create an issue describing the step you need, including:
- The Gherkin syntax youβd like to use
- A short example of the expected behavior
- Any relevant context or use case
-
π€ Contribute directly: If you're comfortable with JavaScript and Cypress, feel free to open a Pull Request. Please:
- Follow the existing step definitions style
- Add Gherkin usage and examples to the README
- Keep tests modular and consistent
π Contributions and feedback are welcome! This runner is designed to be extensible and team-friendly.