Steps
The fixture step
A fixture step writes reference data no API creates, such as country codes, through a connection
declared writes: true, and commits it. It is the one step in a plan that writes to a database on
purpose, and every output says so: vero plan prints a writes: line for each fixture step,
vero run prints a fixture line in the summary for each one that ran, passed or not, and the
event log carries a fixture event with the statement and whether it committed. Seeding an order
this way tests the database insert, not the application's order-creation API; a row the
application should write belongs in an http step. Plan format has the keys every
step shares and the connections block; this page has what a fixture step adds.
| Key | Type | Meaning |
|---|---|---|
connection |
string | a key of connections declared with writes: true |
statement |
string | a literal: one INSERT, UPDATE, DELETE, MERGE or REPLACE |
args |
list of strings | templated, bound as parameters $1, $2, ..., never interpolated |
timeout |
duration | alternative to the step's timeout; not both |
A plan
fixture.yaml migrates a SQLite file beside the plan with an exec
step, seeds it with a fixture step, and reads it back with a sql step. It needs no fixture server.
The same file is declared twice: once read-only for the sql step, once with writes: true for the
fixture step, because a sql step may not read through a writing connection.
connections:
catalog: { driver: sqlite, dsn: catalog.db }
catalog-admin: { driver: sqlite, dsn: catalog.db, writes: true }
tasks:
- name: seed-countries
needs: [migrate]
steps:
- fixture:
connection: catalog-admin
statement: >
insert into countries (code, name) values ($1, $2), ($3, $4)
on conflict (code) do nothing
args: [PT, Portugal, ES, Spain]
assert: [rowsAffected <= 2]
results: { seeded: rowsAffected }
vero plan names the write before anything runs, and a run labels it in the summary:
$ bin/vero plan examples/fixture.yaml
writes: tasks.seed-countries.steps[0] through connection catalog-admin (fixture step, committed)
ok: 3 tasks, 0 finally tasks; the plan sends 0 HTTP requests
$ bin/vero run examples/fixture.yaml
run 01M3ZRZSEA60SAE56SWJT8E0W4 fixture.yaml --jobs 4 lifecycle isolated
fixture seed-countries › step 1 passed through connection catalog-admin: wrote 2 row(s), committed
3 passed 0 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (3 tasks, 0 requests, 0.0s)
Write the statement so that a second run is harmless. This one says on conflict (code) do nothing, so the second run commits and writes no rows:
fixture seed-countries › step 1 passed through connection catalog-admin: wrote 0 row(s), committed
3 passed 0 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (3 tasks, 0 requests, 0.0s)
The assertion rowsAffected <= 2 holds on both runs. One that said rowsAffected == 2 would hold
on the first run only.
What the statement may do
The statement is inspected at load. Its first word is INSERT, UPDATE, DELETE, MERGE or
REPLACE, and it is one statement. Keywords that change schema, permissions, the transaction or
the session are refused anywhere outside a string or a comment: CREATE, DROP, ALTER,
TRUNCATE, GRANT, REVOKE, COPY, ATTACH, DETACH, PRAGMA, BEGIN, COMMIT,
ROLLBACK, SAVEPOINT, RELEASE, CALL, VACUUM, LOCK, SECURITY. The list is shorter than
the sql step's, so on conflict (code) do nothing and a column named comment load. Schema
belongs in a migration run by an exec step, which is what the example's migrate task does.
The statement is a literal. Values go in args, which are templated and bound as parameters
$1, $2, and so on, never interpolated into the text. A {{ in the statement is a load error.
The statement runs in its own transaction, which commits on success and rolls back on failure. On
Postgres that transaction is READ COMMITTED, the isolation reads get; on SQLite the file is
opened as the DSN says, with a 5 s wait on another writer's lock unless the DSN sets its own
busy_timeout. The connection's role is not checked the way a sql connection's is: writing is
what it is for. A fixture step may not repeat. Its deadline is the step's timeout, or
fixture.timeout, 30 s when neither is set; setting both is a load error.
A fixture step may also be a lifecycle.reset step. It then needs a timeout, runs before any
task, and is held to exitCode == 0 when it has no assertions of its own. The writes: line
names it lifecycle.reset[0], and the summary label reads fixture lifecycle.reset › step 1 passed through connection catalog-admin: wrote 1 row(s), committed.
Names in scope
| Name | Meaning |
|---|---|
rowsAffected |
the exact number of rows the statement changed, as the database reports it |
duration.total |
the statement, commit included |
env, tasks, run |
as in every step |
Assertions are evaluated only when the transaction committed. A statement that failed is rolled
back and the step is errored, so rowsAffected == 2 is never checked against a write that did
not happen. A result such as seeded: rowsAffected publishes the count to later tasks.
What a failure shows
A statement the database refuses is rolled back, and the step ends errored. The failure block
prints the statement, every bound argument, and that nothing was written; the summary still
carries the fixture line, now saying rolled back, nothing written, so nobody reading the
report misses that the plan tried to write:
ERROR seed-countries › step 1 fixture.yaml
rolled back: constraint failed: UNIQUE constraint failed: countries.code (1555)
fixture insert into countries (code, name) values ($1, $2), ($3, $4)
$1 = "PT"
$2 = "Portugal"
$3 = "PT"
$4 = "again"
nothing written: rolled back
BLOCKED countries-are-there blocked by seed-countries, which errored
fixture seed-countries › step 1 errored through connection catalog-admin: rolled back, nothing written
1 passed 0 failed 1 errored 0 timed out 0 skipped 1 blocked 0 not run (3 tasks, 0 requests, 0.0s)
A statement the deadline cut off is timed_out, with the reason the statement's deadline fired, and it was rolled back: .... When the write committed and an assertion did not hold, the write
stays committed, the step is failed, and the block says so under the arguments. This is the
example's second run with rowsAffected == 5 in place of rowsAffected <= 2:
FAIL seed-countries › step 1 › assert #1 fixture.yaml:30
fixture insert into countries (code, name) values ($1, $2), ($3, $4) on conflict (code) do nothing
$1 = "PT"
$2 = "Portugal"
$3 = "ES"
$4 = "Spain"
0 row(s) affected, 0.7ms, committed
assert rowsAffected == 5 failed
rowsAffected = 0
fixture.yaml:30
BLOCKED countries-are-there blocked by seed-countries, which failed
fixture seed-countries › step 1 failed through connection catalog-admin: wrote 0 row(s), committed
1 passed 1 failed 0 errored 0 timed out 0 skipped 1 blocked 0 not run (3 tasks, 0 requests, 0.0s)
The event log carries the same write as a fixture event: connection, statement, arguments,
rowsAffected and committed (vero.events/v1).
Load errors
| Plan | Load error |
|---|---|
no connection |
a fixture step needs connection, the name of an entry in connections with writes: true |
| a connection the plan does not declare | connection catalog-admn is not declared in connections (did you mean catalog-admin?) |
a connection without writes: true |
connection catalog is read-only; a fixture step writes through a connection declared with writes: true (ADR 0008) |
no statement |
a fixture step needs a statement |
a {{ in the statement |
a fixture statement is a literal; put values in args, which are bound as parameters, never interpolated |
create table regions (code text) |
a fixture step writes rows: INSERT, UPDATE, DELETE, MERGE or REPLACE, not CREATE. Reads belong in a sql step, schema in a migration run by an exec step (ADR 0008) |
| a refused keyword inside the statement | the statement contains DROP; a fixture step writes rows only and never changes schema, permissions or transactions (ADR 0008) |
| two statements | a fixture step runs one statement; this one has 2. Put each write in its own fixture step |
| a writing connection no fixture step uses | connection catalog-admin is declared but no fixture step uses it |
timeout on the step and inside fixture |
set the timeout once, on the step or inside fixture, not both |
The parenthetical in three of the messages names the decision record that fixed this rule. The rule is the one this page describes.
This page is docs/content/steps/fixture.md in the repository.