This library supports some basic sentences to handle REST API calls and basic database operations.
It is based on Cucumber and helps to support Behaviour-Driven Development (BDD).
Cucumber executes Steps
in form of Gherkin language.
Read also about Anti-Patterns of Cucumber to avoid problems and to have a clear style.
See Changelog for release information.
- Cucumber REST Gherkin library
- Table of content
- Support JUnit 5
- Basic Concept
- Steps
- Database
- REST
- JSON-Unit
- Given
- Set path base directory for request/result/database files
- Set a static value to the context
- Set multiple static values to the context
- Set base path for URLs
- Define that a token without scopes should be used.
- Set a URI path for later execution
- Set a body from JSON file for later execution
- Set a body directly for later execution
- When
- Then
- Extension of JSON Unit Matcher
To support JUnit 5 add the org.junit.vintage:junit-vintage-engine
dependency to your project.
This allows JUnit 5 to execute JUnit 3 and 4 tests.
testRuntimeOnly('org.junit.vintage:junit-vintage-engine') {
because("allows JUnit 3 and JUnit 4 tests to run")
}
This library defines a set of sentences and tries to harmonize them and provide a context-related beginning of sentences. This is very helpful for IDEs with code completion.
Step | Sentence start | Main usage |
---|---|---|
Given |
that |
Prepare something |
When |
executing a or I set |
Do something |
Then |
I ensure or I store |
Validate something |
There are some basic examples in the src/test directory.
It is a best-practice to not use syntax like this:
Scenario:
Given that something was done
Given that something else was done
Given that something more was done
For that Gherkin
offers Steps
like And
to make the definition more readable. In general every sentence can be reused with another Step
.
It is recommended to follow the basic definition like described under Basic concept.
This transforms the upper example to:
Scenario:
Given that something was done
And that something else was done
And that something more was done
This sounds much better, didn't it?
Before every Scenario the library looks for a database/reset_database.xml
file ($projectDir/src/test/resources/database/reset_database.xml
).
This file has to be a Liquibase definition, which can contain everything to reset a database (truncate
, delete
, insert
...).
Scenario:
Given that the database was initialized with the liquibase file {string}
Executes a liquibase script to prepare the database.
Scenario:
Given that the SQL statements from the SQL file {string} was executed
Executes an SQL script to prepare/change the database.
Scenario:
Then I ensure that the result of the query of the file {string} is equal to the CSV file {string}
Executes an SQL query from a file and compares the result in CSV format with the contents of the second file. The conversion of the database result to CSV is done internally.
The REST API steps can be prepared with some given steps.
If a path contains a template placeholder with ${}
like ${elementFromContext}
the library tries to replace this with elementFromContext
in the context, if it exists.
The library contains already two matchers:
${json-unit.matches:isValidDate}
which checks, if the date can be a valid date by parsing it into date formats${json-unit.matches:isEqualToScenarioContext}create_id
which compares the content of the actual JSON to a variable in the ScenarioContext. The context has to be set before with the I store the string of the field "" in the context "" for later usage sentence.
There are more details about how to extend it at the Extension of JSON Unit Matcher section.
For the comparison of the results the library uses JSON
files, which can be enhanced with JSON Unit to validate dynamic responses with things like
- Regex compare
- Ignoring values
- Ignoring elements
- Ignoring paths
- Type placeholders
- Custom matchers
- ...
Scenario:
Given that all file paths are relative to {string}
Sets an internal base file path
for all files in the Scenario
or Feature
.
It is used for:
- Request files
- Response files (for compare)
- Database query files (
.sql
) - Database CSV result files (
.csv
)
Scenario: Test with static key/value added to the context
Given that the context contains the key "staticKey" with the value "staticValue"
And that the API path is "/api/v1/${staticKey}/myApi"
When executing a GET call with previously given URI
or use it in an outline scenario:
Scenario Outline: Test with static key/value added to the context in an outline scenario
Given that the context contains the key "<staticKey>" with the value "<staticValue>"
And that the API path is "/api/v1/${staticURLElement}"
When executing a GET call with previously given URI
Examples:
| staticKey | staticValue |
| staticURLElement | resourceA |
| staticURLElement | resourceB |
| staticURLElement | resourceC |
It adds the key/value pairs to the context. Please ensure, that those static values are unique to avoid overwriting them in later steps.
Scenario: Test with static key/value added to the context via table
Given that the context contains the following 'key' and 'value' pairs
| staticFirstElement | resourceA |
| staticSecondElement | resourceB |
And that the API path is "/api/v1/${staticFirstElement}/${staticSecondElement}"
When executing a GET call with previously given URI
It adds the key/value pairs to the context. Please ensure, that those static values are unique to avoid overwriting them in later steps.
Scenario:
Given that all URLs are relative to {string}
Sets an internal base URL path
for all URLs in the Scenario
or Feature
.
This is very useful to avoid repeating e.g. /api/v1/myapitotest
before every concrete endpoint.
It is also possible to use placeholders with ${placeholder}
syntax. They will be replaced from the context.
Scenario:
Given that a bearer token without scopes is used
This library supports two types of Bearer tokens. With this step it possible to set the token without scope.
Else it uses the default token, which should contain correct scopes
/authorities
.
The token has to be configured in the application.yml
/ application.properties
in the src/main/test/resources
directory.
The token can be set like:
application.properties
:
cucumberTest.authorization.bearerToken.noscope=eyJhbGciOiJI[...]V_adQssw5c
cucumberTest.authorization.bearerToken.default=eyJfgEiooIfS[...]Bs_sadf4de
application.yml
:
cucumberTest:
authorization:
bearerToken:
noscope: eyJhbGciOiJI[...]V_adQssw5c
default: eyJfgEiooIfS[...]Bs_sadf4de
Scenario:
Given that the API path is {string}
This sets a URI, path which can be executed later.
It is required to use this Given
/And
step in cases when it is necessary to manipulate e.g. dynamic elements in the URI.
Scenario:
Given that the file {string} is used as the body
This sets the JSON file for the body for later execution.
It is required to use this Given
step in cases when it is necessary to manipulate e.g. dynamic elements in the URI.
Scenario:
Given that the body of the response is
"""
{
"key": "value"
}
"""
This sets the JSON body for later execution.
It is required to use this Given
step in cases when it is necessary to manipulate e.g. dynamic elements in the URI.
The paths that are used here can be shortened by set a base URL path with Set base path for URLs with a Given
Step before.
Scenario:
When I set the header {string} to {string}
This sentence allows adding or manipulating headers. The first argument is the header name and the second the header value.
Scenario:
When I set the value of the previously given body property {string} to {string}
This can manipulate a previously given body by exchanging a JSON element with the given value.
Requires the Step
Set a body from JSON file for later execution!
A list and description of sentences to execute a request is linked in the next table.
Request type | Link |
---|---|
GET Requests | docs/get_sentences.md |
POST Requests | docs/post_sentences.md |
DELETE Requests | docs/delete_sentences.md |
PUT Requests | docs/put_sentences.md |
PATCH Requests | docs/patch_sentences.md |
Scenario:
Then I ensure that the status code of the response is {int}
Validates, that the response is the expected HTTP code (e.g. 200
).
Scenario:
Then I ensure that the body of the response is equal to the file {string}
Validates, that the body of the response is equal to the given file. Like mentioned above, this file can contain JSON Unit syntax for dynamic field validation.
Scenario:
Then I ensure that the body of the response is equal to
"""
{
"field": "value",
}
"""
In this case, the JSON is written directly under the sentence and enclosed in three double quotation marks. Here it is also possible to use JSON Unit syntax to validate dynamic elements.
Scenario:
Then I store the string of the field {string} in the context {string} for later usage
Attention: This is an Anti-Pattern!
This can be used to write the value of a JSON element of the response to a context that is available through the Feature
or other Scenarios
.
Use this with caution, because at the point where the element is reused, the Scenario
is hard coupled to this Step
which ultimately makes it not executable as single Step
.
On the other hand, this can be useful to support cross-Features
testing with dynamic values for end-to-end testing.
It is possible to extend the JSON matchers by creating a new matcher and extending the org.hamcrest.BaseMatcher
class and implementing the com.ragin.bdd.cucumber.matcher.BddCucumberJsonMatcher
interface.
After they are created, you have to add them to the @ContextConfiguration
classes definition.
See CreateContextHooks.java for an example how the configuration should look like.
A simple matcher to validate the current object as it is, can look like this:
import com.ragin.bdd.cucumber.matcher.BddCucumberJsonMatcher;
import org.apache.commons.lang3.StringUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.springframework.stereotype.Component;
@Component
public class DividableByTwoMatcher extends BaseMatcher<Object> implements BddCucumberJsonMatcher {
public boolean matches(Object item) {
if (StringUtils.isNumeric(String.valueOf(item))) {
// never do that, but it should show something ;)
return Integer.parseInt((String) item) % 2 == 0;
}
return false;
}
@Override
public void describeTo(Description description) {
// nothing to describe here
}
@Override
public String matcherName() {
return "isDividableByTwo";
}
@Override
public Class<? extends BaseMatcher<?>> matcherClass() {
return this.getClass();
}
}
Now you can use this matcher with the following statement in your expected JSON:
{
"number": "${json-unit.matches:isDividableByTwo}"
}
The matcherName()
result is now part of the json-unit.matches:
definition.
If you need parameter, you can implement also the net.javacrumbs.jsonunit.core.ParametrizedMatcher
interface.
If there are several arguments, you can pass the arguments as JSON to the matcher and parse it here.
import com.ragin.bdd.cucumber.matcher.BddCucumberJsonMatcher;
import net.javacrumbs.jsonunit.core.ParametrizedMatcher;
import org.apache.commons.lang3.StringUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.springframework.stereotype.Component;
@Component
public class DividableByNumberMatcher extends BaseMatcher<Object> implements ParametrizedMatcher, BddCucumberJsonMatcher {
private String parameter;
public boolean matches(Object item) {
if (StringUtils.isNumeric(String.valueOf(item))) {
// never do that, but it should show something ;)
return Integer.parseInt((String) item) % Integer.parseInt(parameter) == 0;
}
return false;
}
@Override
public void describeTo(Description description) {
// nothing to describe here
}
@Override
public String matcherName() {
return "isDividableByNumber";
}
@Override
public Class<? extends BaseMatcher<?>> matcherClass() {
return this.getClass();
}
@Override
public void setParameter(String parameter) {
this.parameter = parameter;
}
}
To pass the parameter to the matcher, the JSON has to look like this:
{
"number": "${json-unit.matches:isDividableByNumber}5"
}
If you want to pass a JSON, you have to do it with single quotes:
{
"number": "${json-unit.matches:isDividableByNumber}'{"myarg1": "A"}'"
}