SpecFormula AI
ISABackendInstructions

EntityValidate

Validate database record existence and correctness

EntityValidate lets you validate database records directly in Gherkin, ensuring data is actually written rather than just relying on correct API responses.

Basic Usage

Then should exist a todo, with table:
  | todoId    | userId    | title    | completed |
  | $todo1.id | $Alice.id | Buy milk | false     |

This instruction will:

  1. Map "todo" to the todos table via Entity Spec and retrieve field definitions
  2. Query the table using todoId as primary key
  3. Validate each field value matches the expected value

Why Validate the Database?

ResponseValidate only validates API responses, it cannot confirm whether data is actually written to the database:

# API response might be correct, but database might not have the data
Then create todo(201) response, with table:
  | todoId    | title    |
  | $todo1.id | Buy milk |

# Query database directly to ensure data persistence is correct
And should exist a todo, with table:
  | todoId    | title    |
  | $todo1.id | Buy milk |

Mapping to Entity Spec

The EntityValidate instruction uses Entity Spec to map business entity names to actual database tables, and validates field existence against the DDL.

# entity_to_table_mapping.yml
entity_to_table_mapping:
  - todo: todos
  - user: users
-- schema.sql
CREATE TABLE todos (
    todo_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    title VARCHAR(200) NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Then should exist a todo, with table:
  | todoId    | title    |
  | $todo1.id | Buy milk |

# SpecFormula automatically maps to the todos table
# SELECT * FROM todos WHERE todo_id = ?
# Validate title == "Buy milk"
Gherkin ElementMapping Source
todoentity_to_table_mapping key
todos tableentity_to_table_mapping value
todoId, title fieldsDDL field definitions

JSON DocString Format

Use with json: with """json ... """ to validate JSON-typed field structures and values:

Then should exist an order, with json:
  """json
  {
    "id": $order1.id,
    "orderItems": [
      { "productId": 5, "quantity": 2 },
      { "productId": 6, "quantity": 10 }
    ]
  }
  """

With Symbol System:

Then should exist an order, with json:
  """json
  {
    "id": $order1.id,
    "orderNo": &startsWith("ORD-"),
    "createdAt": &sameTime("2026-01-27T10:00:00"),
    "orderItems": [
      { "productId": $productId, "quantity": &gt(0) }
    ]
  }
  """

JSON format note: Symbol system expressions ($ variables, & CAS constraints, @ time symbols) must NOT be placed inside JSON string quotes "", otherwise they are treated as plain strings.

Escaping Keys Containing .

When a JSON key itself contains a . character, use ["..."] bracket notation to prevent it from being split into nested levels:

Then should exist a setting, with table:
  | id | content.["btn.save"] | content.["btn.cancel"] |
  | 1  | Save                 | Cancel                 |

In JSON DocString, use the original key directly — the framework handles escaping automatically.

Lookup Strategies

EntityValidate supports three query approaches:

1. PK Query (Priority)

Provide the complete primary key to query a single record directly:

| todoId    | title    |
| $todo1.id | Buy milk |

# SELECT * FROM todos WHERE todo_id = ?
# Validate title == "Buy milk"

2. Probe Query

When PK is missing, use provided field combinations as query conditions:

| userId    | status | title    |
| $Alice.id | ACTIVE | Buy milk |

# SELECT * FROM todos WHERE user_id = ? AND status = ? AND title = ?
# Expects exactly one result

3. CAS Post-Filtering

When Probe returns multiple results, use CAS constraints to filter for a unique record:

| userId    | status | createdAt                        |
| $Alice.id | ACTIVE | &sameTime("2026-01-27T10:00:00") |

# SELECT * FROM todos WHERE user_id = ? AND status = ?
# Filter results by created_at ≈ "2026-01-27T10:00:00"

Type Conversion

The framework automatically performs semantic comparison:

Comparison TypeDescription
Numeric50000.00 == 50000 (precision-safe comparison)
TimeTruncated to seconds, ignores millisecond differences

CAS Constraints

CAS constraints can be used for flexible validation:

Then should exist a product, with table:
  | id | name  | price      | stock      | deletedAt |
  | 1  | Phone | &gt(10000) | &lte(1000) | &isNull   |

Common constraints:

  • &isNum, &isStr, &isNull — Type checks
  • &gt(), &lt(), &gte(), &lte(), &between() — Numeric ranges
  • &contains(), &startsWith() — String matching
  • &sameTime() — Time comparison
  • &oneOf() — Enum matching

For the complete list, see CAS Symbols.

Error Handling

Lint Phase

ErrorDescription
Entity not definedNot found in entity_to_table_mapping
Field not foundField not found in table definition

Runtime

ErrorDescription
Record not foundQuery result is empty
Field value mismatchField value doesn't match expected
Multiple resultsProbe returned multiple rows and cannot filter to unique

Complete Example

Feature: Create Todo

  Background:
    Given current time is "2026-01-27T10:00:00"

    # EntitySetup instruction (see EntitySetup docs)
    Given prepare a user, with table:
      | >Alice.id | name  | email             | status |
      | <userId   | Alice | alice@example.com | ACTIVE |

  Example: Validate todo written to database

    # ApiCall instruction (see ApiCall docs)
    When (UID="$Alice.id") create todo, call table:
      | >todo1.id | title    | description    |
      | <todoId   | Buy milk | Go to Costco   |

    # ResponseValidate instruction (see ResponseValidate docs)
    Then create todo(201) response, with table:
      | todoId    | title    |
      | $todo1.id | Buy milk |

    # EntityValidate instruction — validate database record
    # PK query then field-by-field comparison; &sameTime is a CAS constraint
    And should exist a todo, with table:
      | todoId    | userId    | title    | description  | completed | createdAt                        |
      | $todo1.id | $Alice.id | Buy milk | Go to Costco | false     | &sameTime("2026-01-27T10:00:00") |

On this page