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:
- Map "todo" to the
todostable via Entity Spec and retrieve field definitions - Query the table using
todoIdas primary key - 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 Element | Mapping Source |
|---|---|
todo | entity_to_table_mapping key |
todos table | entity_to_table_mapping value |
todoId, title fields | DDL 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": >(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 result3. 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 Type | Description |
|---|---|
| Numeric | 50000.00 == 50000 (precision-safe comparison) |
| Time | Truncated 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 | >(10000) | <e(1000) | &isNull |Common constraints:
&isNum,&isStr,&isNull— Type checks>(),<(),>e(),<e(),&between()— Numeric ranges&contains(),&startsWith()— String matching&sameTime()— Time comparison&oneOf()— Enum matching
For the complete list, see CAS Symbols.
Error Handling
Lint Phase
| Error | Description |
|---|---|
| Entity not defined | Not found in entity_to_table_mapping |
| Field not found | Field not found in table definition |
Runtime
| Error | Description |
|---|---|
| Record not found | Query result is empty |
| Field value mismatch | Field value doesn't match expected |
| Multiple results | Probe 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") |