EntitySetup
Prepare test data with declarative syntax
EntitySetup lets you declare test precondition data directly in Gherkin, without writing INSERT statements in Step Definitions.
Basic Usage
Given prepare a user, with table:
| >Alice.id | name | email | status |
| <userId | Alice | alice@example.com | ACTIVE |This instruction will:
- Map
userto theuserstable via Entity Spec and retrieve column definitions - Insert a record with
name = Alice,email = alice@example.com,status = ACTIVEinto the database - Query back the
user_idcolumn value from database, store in variableAlice.id
Variable Extraction
After INSERT, use > and < pairs to query back auto-generated column values (e.g., auto-increment ID) from the database and store them in variables for subsequent steps:
>variableName(header row): Defines the variable name to store the extracted value<columnName(data row): Specifies the database column name to query back
Given prepare a user, with table:
| >Alice.id | >Alice.createdAt | name | email | status |
| <userId | <createdAt | Alice | alice@example.com | ACTIVE |Execution flow:
- Write
name,email,statusto the database - Query back
userIdcolumn value from database → store in variableAlice.id - Query back
createdAtcolumn value from database → store in variableAlice.createdAt
Variable Reference
Use $variable to reference previously stored variables:
Given prepare a user, with table:
| >Alice.id | name |
| <userId | Alice |
Given prepare a todo, with table:
| userId | title |
| $Alice.id | Buy milk |JSON Field Description
When Entity fields store JSON objects (e.g., PostgreSQL JSONB, MySQL JSON), two methods are available to describe JSON field content.
JSON Path DataTable Header
Use path syntax in DataTable headers to expand JSON structures:
Given prepare an order, with table:
| id | orderItems[0].productId | orderItems[0].quantity | orderItems[1].productId | orderItems[1].quantity |
| 1 | 5 | 2 | 6 | 10 |Equivalent to the compressed single-line string:
Given prepare an order, with table:
| id | orderItems |
| 1 | [{"productId":5,"quantity":2},{"productId":6,"quantity":10}] |With Symbol System:
Given prepare an order, with table:
| id | orderItems[0].productId | orderItems[0].createdAt |
| 1 | $productId | @time(now) |JSON DocString Format
Use with json: with """json ... """ to write structured JSON directly, ideal for deeply nested structures:
Given prepare an order, with json:
"""json
{
"id": 1,
"orderItems": [
{ "productId": 5, "quantity": 2 },
{ "productId": 6, "quantity": 10 }
]
}
"""Multiple records using JSON Array:
Given prepare an order detail, with json:
"""json
[
{ "orderId": 1, "productId": 101, "quantity": 2 },
{ "orderId": 1, "productId": 102, "quantity": 1 }
]
"""With Symbol System:
Given prepare an order, with json:
"""json
{
>orderId: <id,
"orderNo": "ORD-001",
"createdAt": @time(now),
"orderItems": [
{ "productId": $productId, "quantity": 2 }
]
}
"""JSON format note: Symbol system expressions (
$variables,&CAS constraints,@time symbols,>/<extraction keys) 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:
Given prepare 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.
Mapping to Entity Spec
The EntitySetup instruction maps business entity names to actual database tables through Entity Spec, and validates fields against the DDL.
Mapping Mechanism
# entity_to_table_mapping.yml
entity_to_table_mapping:
- user: users
- todo: todos
- order: orders
- product: products-- schema.sql
CREATE TABLE users (
user_id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
status VARCHAR(20) DEFAULT 'ACTIVE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Given prepare a user, with table:
| name | email |
| Alice | alice@example.com |
# SpecFormula automatically maps to users table
# INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')| Gherkin Element | Mapping Source |
|---|---|
user | entity_to_table_mapping key |
users table | entity_to_table_mapping value |
name, email fields | DDL column definition |
Naming Conventions
Entity names: Use Chinese or English names familiar to the business team
# ✅ Recommended: business language
entity_to_table_mapping:
- member: members
- order: orders
- product: products
# ❌ Avoid: technical names
entity_to_table_mapping:
- member_entity: members
- tbl_order: ordersField name conversion: DataTable uses camelCase, automatically mapped to database snake_case
Gherkin: userId → DB: user_id
Gherkin: createdAt → DB: created_atWhen resolving or validating database columns, the framework applies the same three-step strategy for each field name:
- Step 1: exact match
- First, it looks for the original field name as-is, for example
userIdvsuserId.
- First, it looks for the original field name as-is, for example
- Step 2: camelCase → snake_case
- If no exact match is found, it converts
userIdtouser_idand tries to match again against the DDL / actual data.
- If no exact match is found, it converts
- Step 3: case-insensitive
- If the first two steps still fail, it finally falls back to case-insensitive comparison, so
user_id,USER_ID, andUser_Idare treated as the same column.
- If the first two steps still fail, it finally falls back to case-insensitive comparison, so
| Feature field name | DDL / Map field name | Match? | Explanation |
|---|---|---|---|
userId | userId | ✅ Yes | Step 1: direct equals match |
userId | user_id | ✅ Yes | Step 2: camelCase → snake_case |
userId | USER_ID | ✅ Yes | Step 2 to user_id, then Step 3 equalsIgnoreCase |
In practice, as long as you consistently use camelCase in Specs, the engine can robustly resolve both snake_case and upper-case column names from the DDL, without requiring manual handling of case or underscores in SQL or code.
Schema Validation
SpecFormula validates that fields in DataTable exist against the DDL:
# ✅ Correct: fields exist in DDL
Given prepare a user, with table:
| name | email | status |
| Alice | alice@example.com | ACTIVE |
# ❌ Lint error: field does not exist
Given prepare a user, with table:
| name | emailAddress | # emailAddress doesn't exist, should be email
| Alice | alice@example.com |Why Not Use API to Create Precondition Data?
Creating precondition data through API causes test coupling—if the "create user" API has a bug, all tests depending on that user will fail, even if the feature being tested is completely correct.
# ❌ Using API: create user API bug affects all tests
When (No Actor) create user, call table:
| name | email |
| Alice | alice@example.com |
# ✅ EntitySetup: direct database insert, unaffected by API
Given prepare a user, with table:
| name | email |
| Alice | alice@example.com |Type Conversion
Values in DataTable are all strings. The framework auto-converts based on the column type defined in DDL (e.g., INT, DATE, DATETIME):
| DDL Column Type | DataTable Input | Conversion Result |
|---|---|---|
INT / BIGINT | "123" | Integer / Long |
DECIMAL | "99.99" | BigDecimal |
DATE | "2026-01-27" | LocalDate |
DATETIME / TIMESTAMP | "2026-01-27T10:00:00" | LocalDateTime |
DATETIME / TIMESTAMP | @time("now") | Current Mock time |
DATE | @date("now") | Current Mock date |
Error Handling
Lint Phase
| Error | Description |
|---|---|
| Entity not defined | Entity not found in entity_to_table_mapping |
| Field not found | Field in DataTable not found in table definition |
Runtime
| Error | Description |
|---|---|
| INSERT failed | Database constraint violation (unique key conflict, NOT NULL, etc.) |
Complete Example
Feature: Create Todo
Background:
Given current time is "2026-01-27T10:00:00"
# EntitySetup instruction — prepare user data
# >Alice.id / <userId: query back userId after INSERT, store in Alice.id
Given prepare a user, with table:
| >Alice.id | name | email | passwordHash | status |
| <userId | Alice | alice@example.com | password123 | ACTIVE |
Example: Create a todo item
# ApiCall instruction (see ApiCall docs)
When (UID="$Alice.id") create todo, call table:
| >todo1.id | title |
| <todoId | Buy milk |
# ResponseValidate instruction (see ResponseValidate docs)
Then create todo(201) response, with table:
| todoId | userId | title |
| $todo1.id | $Alice.id | Buy milk |