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 setup

  • when - Action to perform

  • then - Expected outcome verification

  • and - Continuation of previous step type

  • but - 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

petId: 123

Query

Add query string parameter

status: "available"

Header

Add HTTP header

header_Authorization: "Bearer {{token}}"

Body (inline)

Set JSON body directly

body: {"name": "Max"}

Body (struct)

Set body with properties

body: + newline + indented properties

BodyFile

Set request body (external file)

bodyFile: "classpath:templates/pet.json"

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:

  1. Extracts default values from the OpenAPI requestBody schema

  2. Merges with user-provided properties (user values take precedence)

  3. 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"

Tags

Tags categorize and filter scenarios. They begin with @ and appear before the element they annotate:

@smoke @critical
scenario: Critical path test
  when: I test
    call ^test

Built-in Tags

  • @ignore - Skip this scenario during execution

  • @wip - Work in progress

  • @slow - Marks slow-running tests

Tag Filtering

Use @BerryCrushTags annotation in Java/Kotlin:

// Exclude @ignore tagged scenarios
@BerryCrushTags(exclude = {"ignore"})

// Only run @smoke tagged scenarios
@BerryCrushTags(include = {"smoke"})

// Combine include and exclude (exclude takes precedence)
@BerryCrushTags(include = {"api"}, exclude = {"slow", "wip"})

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 constraints

  • security - 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.

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

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

{string}

Quoted text

"hello"

{int}

Integer

42

{float}

Decimal

3.14

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

  1. Use descriptive names - Create pet with valid data returns 201 is better than Test1

  2. Group related assertions - Keep assertions in a single then step when verifying one outcome

  3. Use tags for organization - Apply @smoke, @regression, @slow tags consistently

  4. Extract reusable steps to fragments - Authentication flows, common setup/teardown

  5. Leverage background for shared setup - When multiple scenarios need the same preconditions

See Also