Scenario File Syntax
BerryCrush uses a custom scenario DSL (Domain-Specific Language) for defining API tests.
This page covers the complete syntax for .scenario and .fragment files.
File Types
.scenario- Test scenario files containing test definitions.fragment- Reusable step fragments that can be included in scenarios
All files must be UTF-8 encoded.
Basic Scenario
A minimal scenario consists of a name and steps:
scenario: List all pets
when: I request all pets
call ^listPets
then: I receive a 200 response
assert status 200
Step Keywords
Steps begin with keywords that describe their purpose:
given- Precondition setupwhen- Action to performthen- Expected outcome verificationand- Continuation of previous step typebut- Exception or negative case
Example:
scenario: Create and verify pet
given: API is ready
call ^health
assert status 200
when: I create a pet
call ^createPet
body: {"name": "Max", "status": "available"}
then: pet is created
assert status 201
extract $.id => petId
and: I can retrieve it
call ^getPetById
petId: {{petId}}
assert status 200
API Calls
Use call to invoke an OpenAPI operation:
call ^operationId
For multi-spec projects, specify the spec name:
call using auth ^login
Call Parameters
Parameter Type |
Description |
Example |
|---|---|---|
Path |
Replace path variable |
|
Query |
Add query string parameter |
|
Header |
Add HTTP header |
|
Body (inline) |
Set JSON body directly |
|
Body (struct) |
Set body with properties |
|
BodyFile |
Set request body (external file) |
|
Request Body Syntax
The body: keyword supports two modes:
Inline JSON - Specify JSON directly on the same line:
when: I create a pet
call ^createPet
body: {"name": "Fluffy", "status": "available"}
Structured properties - Use indented properties with OpenAPI schema defaults:
when: I create a pet
call ^createPet
body:
name: Fluffy
status: available
The structured body syntax:
Extracts default values from the OpenAPI requestBody schema
Merges with user-provided properties (user values take precedence)
Generates the final JSON body
This allows partial data specification - unspecified fields use schema defaults.
Triple-quoted multi-line body - Use """ for multi-line raw content:
when: I create a pet
call ^createPet
body:
"""
{
"name": "Fluffy",
"status": "available",
"tags": ["cute", "friendly"]
}
"""
This sends the JSON exactly as written (with common indentation stripped).
External Body Files
For large or reusable request bodies, use bodyFile to load content from external files:
when: I create a pet with template
call ^createPet
bodyFile: "classpath:templates/create-pet.json"
Supported path formats:
classpath:path/to/file.json- Load from classpath (recommended)file:./relative/path.json- Load from file system (relative to working directory)/absolute/path.json- Load from absolute file path
Variable interpolation is supported in external body files. Use {{variableName}} syntax:
templates/create-pet.json:
{
"name": "{{petName}}",
"category": "{{category}}",
"status": "available"
}
Variables from previous extractions are automatically substituted when the file is loaded.
Note
If both body and bodyFile are specified, the inline body takes precedence.
Assertions
Status Code Assertions
assert status 200
assert statusCode 201
Body-Level Assertions
Check the response body for content:
# Body contains substring
assert contains "expected text"
# Body does NOT contain substring (negation)
assert not contains "error message"
JSONPath Assertions
Equality:
assert $.name equals "Max"
assert $.id = 123
Pattern Matching (regex):
assert $.email matches ".*@.*\\.com"
assert $.total matches "\\d+"
Array Assertions:
assert $.pets notEmpty
assert $.items size 5
Header Assertions:
# Check header exists
assert header Content-Type
# Check header value
assert header Content-Type = "application/json"
assert header Content-Type: "application/json"
Response Time:
# Response must complete within 5000ms
assert responseTime 5000
Schema Validation:
# Validate response against OpenAPI schema
assert schema
Negating Assertions with not
The not keyword inverts any assertion, making it a negative check.
For body-level assertions (not at the beginning):
assert not contains "error"
For JSONPath assertions (not after the path):
assert $.status not equals "sold"
assert $.items not hasSize 0
assert $.deleted not exists
Both positions are supported to allow natural reading.
Variable Extraction
Extract values from responses for use in subsequent steps:
extract $.id => petId
extract $.token => authToken
Use extracted variables with double curly braces:
call ^getPetById
petId: {{petId}}
Escaping Variable Syntax
To use literal {{ or ${ in strings (without variable interpolation),
escape them with a backslash:
# This asserts the literal text "{{petName}}" appears in the body
assert not contains "\\{{petName}}"
# In request bodies
body: {"template": "Hello \\{{name}}"}
The escape sequences:
\\{{→ literal{{\\$→ literal$
Conditional Assertions
Conditional assertions allow branching logic based on response status, JSON path values, or headers. This is useful when an API can return different success responses.
Basic Syntax
when: I upsert a resource
call ^updatePet
petId: 123
body:
name: "Max"
if status 201
# Resource was created
assert $.id notEmpty
extract $.id => createdId
else if status 200
# Resource was updated
assert $.name equals "Max"
else
fail "Expected status 200 or 201"
Condition Types
Status code:
if status 201
assert $.id notEmpty
JSON path value:
if $.count greaterThan 0
assert $.items[0].id notEmpty
else
# Empty list is acceptable
assert $.items size 0
Header value:
if header Content-Type equals "application/json"
assert $.data notEmpty
else
fail "Expected JSON response"
Nested Conditionals
Conditionals can be nested for complex logic:
if status 200
if $.status equals "available"
assert $.price notEmpty
else
assert $.status notEmpty
else if status 201
assert $.id notEmpty
else
fail "Unexpected status"
The fail Action
Use fail to explicitly fail a scenario with a custom message:
else
fail "Expected status 200 or 201 but got something else"
Features and Background
Features group related scenarios with optional shared setup:
Basic Feature
feature: Pet Store API
scenario: list all pets
when: I list pets
call ^listPets
then: I get results
assert status 200
scenario: create a pet
when: I create a pet
call ^createPet
body: {"name": "Max"}
then: the pet is created
assert status 201
Feature with Background
Background steps run before each scenario in the feature:
feature: Pet Operations
background:
given: setup test data
call ^createPet
body: {"name": "TestPet"}
extract $.id => petId
scenario: get pet by id
when: retrieve the pet
call ^getPetById
petId: {{petId}}
then: pet is returned
assert status 200
scenario: delete pet
when: delete the pet
call ^deletePet
petId: {{petId}}
then: pet is deleted
assert status 204
Tagged Features
Tags on a feature are inherited by all scenarios within:
@api @regression
feature: Authentication Tests
@smoke
scenario: valid login
when: login with credentials
call ^login
then: success
assert status 200
@ignore
scenario: incomplete test
when: TODO
call ^incomplete
In this example:
“valid login” has tags:
api,regression,smoke“incomplete test” has tags:
api,regression,ignore
Parameterized Scenarios (Outline)
Create data-driven tests with scenario outlines:
outline: Create different pets
when: I create a pet
call ^createPet
body: {"name": "{{name}}", "category": "{{category}}"}
then: pet is created
assert status 201
examples:
| name | category |
| Fluffy | cat |
| Buddy | dog |
| Tweety | bird |
This generates 3 scenario runs with different parameter combinations.
Fragment Files
Fragment files (.fragment) define reusable step sequences:
# auth.fragment
fragment: authenticate
given: login with credentials
call ^login
body: {"username": "test", "password": "test"}
assert status 200
extract $.token => authToken
Use fragments in scenarios with include:
scenario: Access protected API
given: I am authenticated
include authenticate
when: I access protected endpoint
call ^getProfile
header_Authorization: "Bearer {{authToken}}"
then: I see my profile
assert status 200
Parameters Block
Parameters can be specified at two levels:
File-Level Parameters
Override default configuration for all scenarios in the file:
parameters:
baseUrl: "http://localhost:8080"
timeout: 60
shareVariablesAcrossScenarios: true
scenario: Test with custom configuration
when: I call the API
call ^listPets
then: I get results
assert status 200
Feature-Level Parameters
Configure only scenarios within a specific feature:
feature: Pet CRUD Operations
parameters:
shareVariablesAcrossScenarios: true
scenario: Create pet
when: I create a pet
call ^createPet
body: {"name": "SharedPet"}
extract $.id => petId
scenario: Use shared variable
# Can access {{petId}} from previous scenario
when: I get the pet
call ^getPetById
petId: {{petId}}
then: I see the pet
assert status 200
Feature-level parameters override file-level parameters for scenarios in that feature. Variables shared within a feature are isolated from other features and standalone scenarios.
Supported Parameters
Auto-Generated Tests
The auto: directive automatically generates invalid request and security tests based on OpenAPI schema constraints.
Basic Syntax
call ^operationId
auto: [<test-types>]
<base-parameters>
Where <test-types> is a space-separated list of:
invalid- Generate tests violating OpenAPI schema constraintssecurity- Generate tests with common attack payloads
Example
scenario: Auto-generated tests for createPet
when: I create a pet with invalid input
call ^createPet
auto: [invalid security]
body:
name: "TestPet"
status: "available"
if status 4xx
# Test passed - invalid request rejected
else
fail "Expected 4xx for {{test.type}}: {{test.description}}"
Context Variables
During auto-test execution, these variables are available:
See Auto-Generated Tests for complete documentation of auto-generated tests.
Custom Steps
Custom steps allow you to extend BerryCrush with reusable, domain-specific steps implemented in Kotlin.
Defining Custom Steps
Use the @Step annotation to define custom steps:
import org.berrycrush.step.Step
import org.berrycrush.step.StepContext
class PetstoreSteps {
@Step("create a test pet named {string}")
fun createTestPet(context: StepContext, name: String) {
val response = context.httpClient.post("/pet") {
json(mapOf("name" to name, "status" to "available"))
}
context.variables["petId"] = response.jsonPath.read<Int>("$.id")
}
}
Step Patterns
Step patterns use placeholders that match values from the scenario:
Placeholder |
Matches |
Example |
|---|---|---|
|
Quoted text |
|
|
Integer |
|
|
Decimal |
|
Using Custom Steps
scenario: Create and verify pet
given: create a test pet named "Fluffy"
then: the pet should have status "available"
Configuration
Register step classes in @BerryCrushConfiguration:
@BerryCrushConfiguration(
openApiSpec = "petstore.yaml",
stepClasses = {PetstoreSteps.class}
)
public class PetstoreScenarioTest {}
Custom Assertions
Custom assertions allow you to extend BerryCrush’s assert directive with domain-specific validation logic.
Defining Custom Assertions
Use the @Assertion annotation to define custom assertions:
import org.berrycrush.assertion.Assertion
import org.berrycrush.assertion.AssertionContext
import org.berrycrush.assertion.AssertionResult
class PetstoreAssertions {
@Assertion("pet name is {string}")
fun assertPetName(context: AssertionContext, expectedName: String): AssertionResult {
val actualName: String? = context.response.jsonPath.read("$.name")
return if (actualName == expectedName) {
AssertionResult.success()
} else {
AssertionResult.failure("Expected name '$expectedName' but got '$actualName'")
}
}
}
Using Custom Assertions
scenario: Verify pet response
when: I get the pet
call ^getPetById
petId: 123
then: the pet exists
assert status 200
assert pet name is "Fluffy"
assert pet is available
Configuration
Register assertion classes in @BerryCrushConfiguration:
@BerryCrushConfiguration(
openApiSpec = "petstore.yaml",
assertionClasses = {PetstoreAssertions.class}
)
public class PetstoreScenarioTest {}
Best Practices
Use descriptive names -
Create pet with valid data returns 201is better thanTest1Group related assertions - Keep assertions in a single
thenstep when verifying one outcomeUse tags for organization - Apply
@smoke,@regression,@slowtags consistentlyExtract reusable steps to fragments - Authentication flows, common setup/teardown
Leverage background for shared setup - When multiple scenarios need the same preconditions
See Also
Auto-Generated Tests - Auto-generated invalid and security tests
File-Level Parameters - Detailed parameter configuration
Fragments - Reusable step fragments
Kotlin DSL - Alternative Kotlin DSL approach
Multi-Spec OpenAPI Support - Working with multiple OpenAPI specs
Comments
Lines starting with
#are comments:# This is a comment describing the test scenario: My test # Explain what this step does when: I do something call ^operation